{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "Posts — msfjarvis.dev",
  "description": "Posts by Harsh Shandilya",
  "home_page_url": "https://msfjarvis.dev/",
  "feed_url": "https://msfjarvis.dev/posts/feed.json",
  "authors": [
    {
      "name": "Harsh Shandilya",
      "url": "https://msfjarvis.dev"
    }
  ],
  "items": [
    {
      "id": "https://msfjarvis.dev/posts/rewriting-claw-html-parser-for-over-the-air-updates/",
      "url": "https://msfjarvis.dev/posts/rewriting-claw-html-parser-for-over-the-air-updates/",
      "title": "Rewriting Claw's HTML parser for over-the-air updates",
      "date_published": "2026-07-17T13:34:00.000Z",
      "summary": "Struggling through a brand new framework so I can hotfix my app faster",
      "content_html": "<div class=\"markdown-alert markdown-alert-note\" dir=\"auto\">\n<p class=\"markdown-alert-title\" dir=\"auto\"><svg class=\"octicon\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>NOTE</p>\n<p>I used LLMs to help debug some of the code explained below, and ended up throwing away a lot of it while re-reviewing that work as part of writing this post. People have strong opinions on the use of LLMs and I don’t want them to feel misled by discovering partway through that this work involved an LLM, so here’s your PSA.</p>\n</div>\n<p>First, some background. Claw is an Android app for browsing the linkblogging community known as <a href=\"https://lobste.rs\">Lobsters</a>. For years, the app relied on a JSON API to interact with the site. Some months ago, I came across <a href=\"https://github.com/lobsters/lobsters/issues/1663#issuecomment-3074472781\">a GitHub comment</a> from the site admin explaining that the JSON API is considered a Ruby on Rails misfeature and not something he supports for external integrations. I don’t expect it to ever fully disappear since many other third-party integrations rely on it, but I decided I should probably look into options anyway. I <a href=\"https://github.com/msfjarvis/compose-lobsters/issues/929\">filed an issue</a> with my initial findings then pretty much forgot about it for over half a year.</p>\n<hr>\n<h2 id=\"the-problems-with-html-parsing\">The problems with HTML parsing</h2>\n<p>In May 2026 I decided to revisit the problem and make some progress on this front, since I had also added the ability to log into the site through the app now and wanted to start offering more interactive features that would need parsing HTML anyway. The result of it was <a href=\"https://github.com/msfjarvis/compose-lobsters/pull/1162\">this massive PR</a> where my ability to comprehend CSS selectors was tested repeatedly and I did not exactly score passing marks. Unsurprisingly, I broke some things<sub><a href=\"https://github.com/msfjarvis/compose-lobsters/commit/875b41d0bfa5be82c34807c07f6f7b03877f0a87\">[1]</a> <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/1b6ae68e0d4a27d99709a09b7ef759f6bcda5b17\">[2]</a></sub> in the process, but it all came together in the end, and I shipped these changes towards the end of that month.</p>\n<p>Anybody who has had to write a scraper can already see the next line coming: Lobsters made a change to the site markup exactly 2 days after I released that code. I scrambled to <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/7ec580d9c63d56fc863690dbb440a9a9e85bcf36\">release a fix</a> (shoutout to Firefox DevTools for introducing me to this horror: <code>div.byline &gt; a[href^=/~]:not([tabindex]):not([aria-hidden=true])</code>), which made me think there had to be a better solution to this problem that didn’t involve waiting hours or days for Google Play Store.</p>\n<h2 id=\"looking-for-a-faster-deployment-model\">Looking for a faster deployment model</h2>\n<p>The React Native folks have had this solved for years through <a href=\"https://expo.dev\">Expo</a>, which offers a cloud-based service called <a href=\"https://docs.expo.dev/eas-update/introduction/\">EAS Update</a> that can push updated JavaScript, assets and images to an existing build of a React Native app without needing to go through an app store. How do we bring this to native Android apps?</p>\n<p>This is the question that Cash App seems to have asked themselves as well, and their answer is called <a href=\"https://github.com/cashapp/Zipline\">Zipline</a>. It’s a Gradle plugin and Kotlin Multiplatform library that enables you to run Kotlin/JS libraries on Kotlin/JVM and Kotlin/Native apps, using the <a href=\"https://bellard.org/quickjs/\">QuickJS</a> embeddable JavaScript engine. If you can compile your code to JavaScript, you can leverage Zipline to update it over the air in a reliable and secure manner.</p>\n<p>Zipline offered everything I was looking for, and since I was predominantly using Kotlin/JS-capable libraries for the HTML parsing it seemed like a no-brainer to give this a go. What follows is a deep dive through false leads about code size, stack overflows, and cache invalidation.</p>\n<h2 id=\"the-great-zipline-migration\">The great Zipline migration</h2>\n<div class=\"markdown-alert markdown-alert-warning\" dir=\"auto\">\n<p class=\"markdown-alert-title\" dir=\"auto\"><svg class=\"octicon\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path></svg>WARNING</p>\n<p>This is a novice stumbling his way through an unfamiliar framework and paradigm, who will unsurprisingly be misled by a clanker and make a heroic recovery. Please exercise caution/patience.</p>\n</div>\n<p>My dependencies were all ready for Kotlin/JS, but my own code was woefully Android-specific and needed some refactoring. Before I started on this, I had to first decide on how I wanted to split the responsibilities between the K/JS “guest” and the Android “host”. This is what I ended up with:</p>\n<style>\n  .mermaid-modal {\n    margin: 1.5rem 0;\n  }\n\n  .mermaid-modal__figure {\n    margin: 0;\n  }\n\n  .mermaid-modal__preview {\n    display: block;\n    overflow-x: auto;\n  }\n\n  .mermaid-modal__preview svg {\n    display: block;\n    max-width: 100%;\n    height: auto;\n    margin-inline: auto;\n  }\n\n  .mermaid-modal__actions {\n    display: flex;\n    justify-content: center;\n    margin-top: 0.75rem;\n  }\n\n  .mermaid-modal__open-button {\n    appearance: none;\n    border: 1px solid var(--border);\n    background: var(--bg-subtle);\n    color: var(--text);\n    border-radius: 999px;\n    padding: 0.45rem 0.8rem;\n    font: inherit;\n    cursor: pointer;\n  }\n\n  .mermaid-modal__open-button:hover {\n    border-color: var(--accent);\n    color: var(--accent);\n  }\n\n  .mermaid-modal__dialog {\n    position: fixed;\n    inset: 0;\n    z-index: 40000;\n    display: grid;\n    place-items: center;\n    padding: 1.5rem;\n    opacity: 0;\n    visibility: hidden;\n    pointer-events: none;\n    transition: opacity 220ms ease, visibility 220ms ease;\n  }\n\n  .mermaid-modal__dialog[data-open='true'] {\n    opacity: 1;\n    visibility: visible;\n    pointer-events: auto;\n  }\n\n  .mermaid-modal__overlay {\n    position: absolute;\n    inset: 0;\n    border: 0;\n    background: rgb(0 0 0 / 0.72);\n    backdrop-filter: blur(10px);\n    cursor: pointer;\n  }\n\n  .mermaid-modal__sheet {\n    position: relative;\n    width: min(100%, 1400px);\n    max-height: calc(100vh - 3rem);\n    display: grid;\n    gap: 0.85rem;\n    transform: translateY(1rem) scale(0.985);\n    opacity: 0;\n    transition: transform 260ms ease, opacity 260ms ease;\n  }\n\n  .mermaid-modal__dialog[data-open='true'] .mermaid-modal__sheet {\n    transform: translateY(0) scale(1);\n    opacity: 1;\n  }\n\n  .mermaid-modal__content {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 100%;\n  }\n\n  .mermaid-modal__content svg {\n    display: block;\n    width: min(100%, 1400px);\n    max-width: 100%;\n    max-height: calc(100vh - 8rem);\n    height: auto;\n    margin-inline: auto;\n  }\n\n  .mermaid-modal__close {\n    position: absolute;\n    top: 0.75rem;\n    right: 0.75rem;\n    z-index: 1;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    border: 1px solid color-mix(in srgb, white 24%, transparent);\n    border-radius: 999px;\n    background: rgb(17 17 17 / 0.55);\n    color: white;\n    padding: 0.6rem;\n    cursor: pointer;\n    backdrop-filter: blur(12px);\n  }\n\n  .mermaid-modal__close:focus-visible,\n  .mermaid-modal__open-button:focus-visible {\n    outline: 2px solid var(--accent);\n    outline-offset: 2px;\n  }\n</style><img src=\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4NTkuMDQ2IiBoZWlnaHQ9IjgxOC42IiB2aWV3Qm94PSItNTAgLTEwIDg1OS4wNDYgODE4LjYiIHN0eWxlPSJjb2xvci1zY2hlbWU6bGlnaHQgZGFyayIgY2xhc3M9Im1lcm1haWQtZGlhZ3JhbSI+PHJlY3QgeD0iLTUwIiB5PSItMTAiIHdpZHRoPSI4NTkuMDQ2IiBoZWlnaHQ9IjgxOC42IiBmaWxsPSIjZmZmZmZmIj48L3JlY3Q+PGRlZnM+PG1hcmtlciBpZD0iYXJyb3ctMCIgdmlld0JveD0iMCAwIDEwIDEwIiByZWZYPSI1IiByZWZZPSI1IiBtYXJrZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIG1hcmtlcldpZHRoPSI4IiBtYXJrZXJIZWlnaHQ9IjgiIG9yaWVudD0iYXV0byI+PHBhdGggZD0iTSAwIDAgTCAxMCA1IEwgMCAxMCB6IiBmaWxsPSIjNDQ0NDQ0IiBzdHJva2U9IiM0NDQ0NDQiIHN0cm9rZS13aWR0aD0iMSIgc3Ryb2tlLWRhc2hhcnJheT0iMSAwIj48L3BhdGg+PC9tYXJrZXI+PG1hcmtlciBpZD0iYXJyb3ctc3RhcnQtMCIgdmlld0JveD0iMCAwIDEwIDEwIiByZWZYPSI0LjUiIHJlZlk9IjUiIG1hcmtlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgbWFya2VyV2lkdGg9IjgiIG1hcmtlckhlaWdodD0iOCIgb3JpZW50PSJhdXRvIj48cGF0aCBkPSJNIDAgNSBMIDEwIDEwIEwgMTAgMCB6IiBmaWxsPSIjNDQ0NDQ0IiBzdHJva2U9IiM0NDQ0NDQiIHN0cm9rZS13aWR0aD0iMSIgc3Ryb2tlLWRhc2hhcnJheT0iMSAwIj48L3BhdGg+PC9tYXJrZXI+PG1hcmtlciBpZD0iYXJyb3ctc2VxLTAiIHZpZXdCb3g9Ii0xIDAgMTIgMTAiIHJlZlg9IjcuOSIgcmVmWT0iNSIgbWFya2VyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBtYXJrZXJXaWR0aD0iMTIiIG1hcmtlckhlaWdodD0iMTIiIG9yaWVudD0iYXV0by1zdGFydC1yZXZlcnNlIj48cGF0aCBkPSJNIC0xIDAgTCAxMCA1IEwgMCAxMCB6IiBmaWxsPSIjNDQ0NDQ0IiBzdHJva2U9IiM0NDQ0NDQiIHN0cm9rZS13aWR0aD0iMSIgc3Ryb2tlLWRhc2hhcnJheT0iMSAwIj48L3BhdGg+PC9tYXJrZXI+PG1hcmtlciBpZD0iYXJyb3ctc3RhcnQtc2VxLTAiIHZpZXdCb3g9Ii0xIDAgMTIgMTAiIHJlZlg9IjIuMSIgcmVmWT0iNSIgbWFya2VyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBtYXJrZXJXaWR0aD0iMTIiIG1hcmtlckhlaWdodD0iMTIiIG9yaWVudD0iYXV0byI+PHBhdGggZD0iTSAxMSAwIEwgMCA1IEwgMTEgMTAgeiIgZmlsbD0iIzQ0NDQ0NCIgc3Ryb2tlPSIjNDQ0NDQ0IiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1kYXNoYXJyYXk9IjEgMCI+PC9wYXRoPjwvbWFya2VyPjwvZGVmcz48bGluZSB4MT0iNzUuMDAiIHkxPSI2NS4wMCIgeDI9Ijc1LjAwIiB5Mj0iNzMyLjYwIiBzdHJva2U9IiM0NDQ0NDQiIHN0cm9rZS13aWR0aD0iMC41Ij48L2xpbmU+PGxpbmUgeDE9IjI3NS4wMCIgeTE9IjY1LjAwIiB4Mj0iMjc1LjAwIiB5Mj0iNzMyLjYwIiBzdHJva2U9IiM0NDQ0NDQiIHN0cm9rZS13aWR0aD0iMC41Ij48L2xpbmU+PGxpbmUgeDE9IjQ4NC4wNSIgeTE9IjY1LjAwIiB4Mj0iNDg0LjA1IiB5Mj0iNzMyLjYwIiBzdHJva2U9IiM0NDQ0NDQiIHN0cm9rZS13aWR0aD0iMC41Ij48L2xpbmU+PGxpbmUgeDE9IjY4NC4wNSIgeTE9IjY1LjAwIiB4Mj0iNjg0LjA1IiB5Mj0iNzMyLjYwIiBzdHJva2U9IiM0NDQ0NDQiIHN0cm9rZS13aWR0aD0iMC41Ij48L2xpbmU+PHBhdGggZD0iTSAyNTAuMDAgNzUuMDAgTCA0OTcuODUgNzUuMDAgTCA1MDkuMDUgODYuMjAgTCA1MDkuMDUgMTE0LjQwIEwgMjUwLjAwIDExNC40MCBaIiBmaWxsPSIjZTBjOGRiIiBzdHJva2U9IiM3YTNkNmUiIHN0cm9rZS13aWR0aD0iMS4xIj48L3BhdGg+PHBvbHlsaW5lIHBvaW50cz0iNDk3Ljg1LDc1LjAwIDQ5Ny44NSw4Ni4yMCA1MDkuMDUsODYuMjAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzdhM2Q2ZSIgc3Ryb2tlLXdpZHRoPSIxLjAiPjwvcG9seWxpbmU+PHRleHQgeD0iMzc5LjUyIiB5PSI5OC4yMCIgZmlsbD0iIzExMTExMSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkludGVyLHVpLXNhbnMtc2VyaWYsc3lzdGVtLXVpLC1hcHBsZS1zeXN0ZW0sU2Vnb2UgVUksRGVqYVZ1IFNhbnMsTGliZXJhdGlvbiBTYW5zLHNhbnMtc2VyaWYsTm90byBDb2xvciBFbW9qaSxBcHBsZSBDb2xvciBFbW9qaSxTZWdvZSBVSSBFbW9qaSIgZm9udC1zaXplPSIxNCI+PHRzcGFuIHg9IjM3OS41MiIgZHk9IjAuMDAiPk9uZS10aW1lIHNldHVwPC90c3Bhbj48L3RleHQ+PHBhdGggZD0iTSA1MC4wMCAyODEuOTAgTCA0OTcuODUgMjgxLjkwIEwgNTA5LjA1IDI5My4xMCBMIDUwOS4wNSAzNDUuMzAgTCA1MC4wMCAzNDUuMzAgWiIgZmlsbD0iI2UwYzhkYiIgc3Ryb2tlPSIjN2EzZDZlIiBzdHJva2Utd2lkdGg9IjEuMSI+PC9wYXRoPjxwb2x5bGluZSBwb2ludHM9IjQ5Ny44NSwyODEuOTAgNDk3Ljg1LDI5My4xMCA1MDkuMDUsMjkzLjEwIiBmaWxsPSJub25lIiBzdHJva2U9IiM3YTNkNmUiIHN0cm9rZS13aWR0aD0iMS4wIj48L3BvbHlsaW5lPjx0ZXh0IHg9IjI3OS41MiIgeT0iMzA2LjYwIiBmaWxsPSIjMTExMTExIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSW50ZXIsdWktc2Fucy1zZXJpZixzeXN0ZW0tdWksLWFwcGxlLXN5c3RlbSxTZWdvZSBVSSxEZWphVnUgU2FucyxMaWJlcmF0aW9uIFNhbnMsc2Fucy1zZXJpZixOb3RvIENvbG9yIEVtb2ppLEFwcGxlIENvbG9yIEVtb2ppLFNlZ29lIFVJIEVtb2ppIiBmb250LXNpemU9IjE0Ij48dHNwYW4geD0iMjc5LjUyIiBkeT0iMC4wMCI+UGVyLXJlcXVlc3Q8L3RzcGFuPjx0c3BhbiB4PSIyNzkuNTIiIGR5PSIyMS4wMCI+cGFyc2luZyBmbG93PC90c3Bhbj48L3RleHQ+PHBhdGggZD0iTSAwLjAwIDM1NS4zMCBMIDEzOC44MCAzNTUuMzAgTCAxNTAuMDAgMzY2LjUwIEwgMTUwLjAwIDM5NC43MCBMIDAuMDAgMzk0LjcwIFoiIGZpbGw9IiNlMGM4ZGIiIHN0cm9rZT0iIzdhM2Q2ZSIgc3Ryb2tlLXdpZHRoPSIxLjEiPjwvcGF0aD48cG9seWxpbmUgcG9pbnRzPSIxMzguODAsMzU1LjMwIDEzOC44MCwzNjYuNTAgMTUwLjAwLDM2Ni41MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjN2EzZDZlIiBzdHJva2Utd2lkdGg9IjEuMCI+PC9wb2x5bGluZT48dGV4dCB4PSI3NS4wMCIgeT0iMzc4LjUwIiBmaWxsPSIjMTExMTExIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSW50ZXIsdWktc2Fucy1zZXJpZixzeXN0ZW0tdWksLWFwcGxlLXN5c3RlbSxTZWdvZSBVSSxEZWphVnUgU2FucyxMaWJlcmF0aW9uIFNhbnMsc2Fucy1zZXJpZixOb3RvIENvbG9yIEVtb2ppLEFwcGxlIENvbG9yIEVtb2ppLFNlZ29lIFVJIEVtb2ppIiBmb250LXNpemU9IjE0Ij48dHNwYW4geD0iNzUuMDAiIGR5PSIwLjAwIj5GZXRjaCBIVE1MPC90c3Bhbj48L3RleHQ+PHBhdGggZD0iTSAwLjAwIDY3MS4yMCBMIDEzOC44MCA2NzEuMjAgTCAxNTAuMDAgNjgyLjQwIEwgMTUwLjAwIDcxMC42MCBMIDAuMDAgNzEwLjYwIFoiIGZpbGw9IiNlMGM4ZGIiIHN0cm9rZT0iIzdhM2Q2ZSIgc3Ryb2tlLXdpZHRoPSIxLjEiPjwvcGF0aD48cG9seWxpbmUgcG9pbnRzPSIxMzguODAsNjcxLjIwIDEzOC44MCw2ODIuNDAgMTUwLjAwLDY4Mi40MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjN2EzZDZlIiBzdHJva2Utd2lkdGg9IjEuMCI+PC9wb2x5bGluZT48dGV4dCB4PSI3NS4wMCIgeT0iNjk0LjQwIiBmaWxsPSIjMTExMTExIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSW50ZXIsdWktc2Fucy1zZXJpZixzeXN0ZW0tdWksLWFwcGxlLXN5c3RlbSxTZWdvZSBVSSxEZWphVnUgU2FucyxMaWJlcmF0aW9uIFNhbnMsc2Fucy1zZXJpZixOb3RvIENvbG9yIEVtb2ppLEFwcGxlIENvbG9yIEVtb2ppLFNlZ29lIFVJIEVtb2ppIiBmb250LXNpemU9IjE0Ij48dHNwYW4geD0iNzUuMDAiIGR5PSIwLjAwIj5SZW5kZXIgVUk8L3RzcGFuPjwvdGV4dD48cGF0aCBpZD0iZWRnZS0wIiBkPSJNIDI3NS4wMDAsMTc5LjkwMCBMIDY4NC4wNDYsMTc5LjkwMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNDQ0NDQ0IiBjbGFzcz0iZWRnZVBhdGgiIGRhdGEtZWRnZS1pZD0iZWRnZS0wIiBzdHJva2Utd2lkdGg9IjEuNSIgbWFya2VyLWVuZD0idXJsKCNhcnJvdy1zZXEtMCkiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PC9wYXRoPjxyZWN0IHg9IjMzNC4wNyIgeT0iMTIyLjcwIiB3aWR0aD0iMTQ2LjQzIiBoZWlnaHQ9IjUyLjAwIiByeD0iMiIgcnk9IjIiIGZpbGw9IiNmZmZmZmYiIHN0cm9rZT0iI2U4ZThlOCIgY2xhc3M9ImVkZ2VMYWJlbCBzZXF1ZW5jZUVkZ2VMYWJlbCIgZGF0YS1lZGdlLWlkPSJlZGdlLTAiIGRhdGEtbGFiZWwta2luZD0iY2VudGVyIiBmaWxsLW9wYWNpdHk9IjAiIHN0cm9rZS1vcGFjaXR5PSIwIiBzdHJva2Utd2lkdGg9IjAuOCI+PC9yZWN0PjxnIGNsYXNzPSJlZGdlTGFiZWwiIGRhdGEtZWRnZS1pZD0iZWRnZS0wIiBkYXRhLWxhYmVsLWtpbmQ9ImNlbnRlciI+PHRleHQgeD0iNDA3LjI5IiB5PSIxNDEuNzAiIGZpbGw9IiMxMTExMTEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJJbnRlcix1aS1zYW5zLXNlcmlmLHN5c3RlbS11aSwtYXBwbGUtc3lzdGVtLFNlZ29lIFVJLERlamFWdSBTYW5zLExpYmVyYXRpb24gU2FucyxzYW5zLXNlcmlmLE5vdG8gQ29sb3IgRW1vamksQXBwbGUgQ29sb3IgRW1vamksU2Vnb2UgVUkgRW1vamkiIGZvbnQtc2l6ZT0iMTQiPjx0c3BhbiB4PSI0MDcuMjkiIGR5PSIwLjAwIj5GZXRjaCBtYW5pZmVzdDwvdHNwYW4+PHRzcGFuIHg9IjQwNy4yOSIgZHk9IjIxLjAwIj5hbmQgbW9kdWxlczwvdHNwYW4+PC90ZXh0PjwvZz48cGF0aCBpZD0iZWRnZS0xIiBkPSJNIDY4NC4wNDYsMjI1LjkwMCBMIDI3NS4wMDAsMjI1LjkwMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNDQ0NDQ0IiBjbGFzcz0iZWRnZVBhdGgiIGRhdGEtZWRnZS1pZD0iZWRnZS0xIiBzdHJva2Utd2lkdGg9IjEuNSIgbWFya2VyLWVuZD0idXJsKCNhcnJvdy1zZXEtMCkiIHN0cm9rZS1kYXNoYXJyYXk9IjMgMyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48L3BhdGg+PHJlY3QgeD0iMzQ0LjAwIiB5PSIxOTUuMTAiIHdpZHRoPSIxMzYuNTQiIGhlaWdodD0iMjguMDAiIHJ4PSIyIiByeT0iMiIgZmlsbD0iI2ZmZmZmZiIgc3Ryb2tlPSIjZThlOGU4IiBjbGFzcz0iZWRnZUxhYmVsIHNlcXVlbmNlRWRnZUxhYmVsIiBkYXRhLWVkZ2UtaWQ9ImVkZ2UtMSIgZGF0YS1sYWJlbC1raW5kPSJjZW50ZXIiIGZpbGwtb3BhY2l0eT0iMCIgc3Ryb2tlLW9wYWNpdHk9IjAiIHN0cm9rZS13aWR0aD0iMC44Ij48L3JlY3Q+PGcgY2xhc3M9ImVkZ2VMYWJlbCIgZGF0YS1lZGdlLWlkPSJlZGdlLTEiIGRhdGEtbGFiZWwta2luZD0iY2VudGVyIj48dGV4dCB4PSI0MTIuMjciIHk9IjIxMi42MCIgZmlsbD0iIzExMTExMSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkludGVyLHVpLXNhbnMtc2VyaWYsc3lzdGVtLXVpLC1hcHBsZS1zeXN0ZW0sU2Vnb2UgVUksRGVqYVZ1IFNhbnMsTGliZXJhdGlvbiBTYW5zLHNhbnMtc2VyaWYsTm90byBDb2xvciBFbW9qaSxBcHBsZSBDb2xvciBFbW9qaSxTZWdvZSBVSSBFbW9qaSIgZm9udC1zaXplPSIxNCI+PHRzcGFuIHg9IjQxMi4yNyIgZHk9IjAuMDAiPkJ1bmRsZSBhc3NldHM8L3RzcGFuPjwvdGV4dD48L2c+PHBhdGggaWQ9ImVkZ2UtMiIgZD0iTSAyNzUuMDAwLDI3MS45MDAgTCA0ODQuMDQ2LDI3MS45MDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzQ0NDQ0NCIgY2xhc3M9ImVkZ2VQYXRoIiBkYXRhLWVkZ2UtaWQ9ImVkZ2UtMiIgc3Ryb2tlLXdpZHRoPSIxLjUiIG1hcmtlci1lbmQ9InVybCgjYXJyb3ctc2VxLTApIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjwvcGF0aD48cmVjdCB4PSIyOTEuNDgiIHk9IjI0MS4xMCIgd2lkdGg9IjE3Ni4wOSIgaGVpZ2h0PSIyOC4wMCIgcng9IjIiIHJ5PSIyIiBmaWxsPSIjZmZmZmZmIiBzdHJva2U9IiNlOGU4ZTgiIGNsYXNzPSJlZGdlTGFiZWwgc2VxdWVuY2VFZGdlTGFiZWwiIGRhdGEtZWRnZS1pZD0iZWRnZS0yIiBkYXRhLWxhYmVsLWtpbmQ9ImNlbnRlciIgZmlsbC1vcGFjaXR5PSIwIiBzdHJva2Utb3BhY2l0eT0iMCIgc3Ryb2tlLXdpZHRoPSIwLjgiPjwvcmVjdD48ZyBjbGFzcz0iZWRnZUxhYmVsIiBkYXRhLWVkZ2UtaWQ9ImVkZ2UtMiIgZGF0YS1sYWJlbC1raW5kPSJjZW50ZXIiPjx0ZXh0IHg9IjM3OS41MiIgeT0iMjU4LjYwIiBmaWxsPSIjMTExMTExIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSW50ZXIsdWktc2Fucy1zZXJpZixzeXN0ZW0tdWksLWFwcGxlLXN5c3RlbSxTZWdvZSBVSSxEZWphVnUgU2FucyxMaWJlcmF0aW9uIFNhbnMsc2Fucy1zZXJpZixOb3RvIENvbG9yIEVtb2ppLEFwcGxlIENvbG9yIEVtb2ppLFNlZ29lIFVJIEVtb2ppIiBmb250LXNpemU9IjE0Ij48dHNwYW4geD0iMzc5LjUyIiBkeT0iMC4wMCI+SW5pdGlhbGl6ZSBwYXJzZXI8L3RzcGFuPjwvdGV4dD48L2c+PHBhdGggaWQ9ImVkZ2UtMyIgZD0iTSA3NS4wMDAsNDQwLjcwMCBMIDI3NS4wMDAsNDQwLjcwMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNDQ0NDQ0IiBjbGFzcz0iZWRnZVBhdGgiIGRhdGEtZWRnZS1pZD0iZWRnZS0zIiBzdHJva2Utd2lkdGg9IjEuNSIgbWFya2VyLWVuZD0idXJsKCNhcnJvdy1zZXEtMCkiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PC9wYXRoPjxyZWN0IHg9IjExNi42MiIgeT0iNDA5LjkwIiB3aWR0aD0iMTE2Ljc3IiBoZWlnaHQ9IjI4LjAwIiByeD0iMiIgcnk9IjIiIGZpbGw9IiNmZmZmZmYiIHN0cm9rZT0iI2U4ZThlOCIgY2xhc3M9ImVkZ2VMYWJlbCBzZXF1ZW5jZUVkZ2VMYWJlbCIgZGF0YS1lZGdlLWlkPSJlZGdlLTMiIGRhdGEtbGFiZWwta2luZD0iY2VudGVyIiBmaWxsLW9wYWNpdHk9IjAiIHN0cm9rZS1vcGFjaXR5PSIwIiBzdHJva2Utd2lkdGg9IjAuOCI+PC9yZWN0PjxnIGNsYXNzPSJlZGdlTGFiZWwiIGRhdGEtZWRnZS1pZD0iZWRnZS0zIiBkYXRhLWxhYmVsLWtpbmQ9ImNlbnRlciI+PHRleHQgeD0iMTc1LjAwIiB5PSI0MjcuNDAiIGZpbGw9IiMxMTExMTEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJJbnRlcix1aS1zYW5zLXNlcmlmLHN5c3RlbS11aSwtYXBwbGUtc3lzdGVtLFNlZ29lIFVJLERlamFWdSBTYW5zLExpYmVyYXRpb24gU2FucyxzYW5zLXNlcmlmLE5vdG8gQ29sb3IgRW1vamksQXBwbGUgQ29sb3IgRW1vamksU2Vnb2UgVUkgRW1vamkiIGZvbnQtc2l6ZT0iMTQiPjx0c3BhbiB4PSIxNzUuMDAiIGR5PSIwLjAwIj5wYXJzZShodG1sKTwvdHNwYW4+PC90ZXh0PjwvZz48cGF0aCBpZD0iZWRnZS00IiBkPSJNIDI3NS4wMDAsNTA2LjIwMCBMIDQ4NC4wNDYsNTA2LjIwMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNDQ0NDQ0IiBjbGFzcz0iZWRnZVBhdGgiIGRhdGEtZWRnZS1pZD0iZWRnZS00IiBzdHJva2Utd2lkdGg9IjEuNSIgbWFya2VyLWVuZD0idXJsKCNhcnJvdy1zZXEtMCkiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PC9wYXRoPjxyZWN0IHg9IjMyMS4xNCIgeT0iNDQ5LjAwIiB3aWR0aD0iMTE2Ljc3IiBoZWlnaHQ9IjUyLjAwIiByeD0iMiIgcnk9IjIiIGZpbGw9IiNmZmZmZmYiIHN0cm9rZT0iI2U4ZThlOCIgY2xhc3M9ImVkZ2VMYWJlbCBzZXF1ZW5jZUVkZ2VMYWJlbCIgZGF0YS1lZGdlLWlkPSJlZGdlLTQiIGRhdGEtbGFiZWwta2luZD0iY2VudGVyIiBmaWxsLW9wYWNpdHk9IjAiIHN0cm9rZS1vcGFjaXR5PSIwIiBzdHJva2Utd2lkdGg9IjAuOCI+PC9yZWN0PjxnIGNsYXNzPSJlZGdlTGFiZWwiIGRhdGEtZWRnZS1pZD0iZWRnZS00IiBkYXRhLWxhYmVsLWtpbmQ9ImNlbnRlciI+PHRleHQgeD0iMzc5LjUyIiB5PSI0NjguMDAiIGZpbGw9IiMxMTExMTEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJJbnRlcix1aS1zYW5zLXNlcmlmLHN5c3RlbS11aSwtYXBwbGUtc3lzdGVtLFNlZ29lIFVJLERlamFWdSBTYW5zLExpYmVyYXRpb24gU2FucyxzYW5zLXNlcmlmLE5vdG8gQ29sb3IgRW1vamksQXBwbGUgQ29sb3IgRW1vamksU2Vnb2UgVUkgRW1vamkiIGZvbnQtc2l6ZT0iMTQiPjx0c3BhbiB4PSIzNzkuNTIiIGR5PSIwLjAwIj5CcmlkZ2U8L3RzcGFuPjx0c3BhbiB4PSIzNzkuNTIiIGR5PSIyMS4wMCI+cGFyc2UoaHRtbCk8L3RzcGFuPjwvdGV4dD48L2c+PHBhdGggaWQ9ImVkZ2UtNSIgZD0iTSA0ODQuMDQ2LDU5NS43MDAgTCAyNzUuMDAwLDU5NS43MDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzQ0NDQ0NCIgY2xhc3M9ImVkZ2VQYXRoIiBkYXRhLWVkZ2UtaWQ9ImVkZ2UtNSIgc3Ryb2tlLXdpZHRoPSIxLjUiIG1hcmtlci1lbmQ9InVybCgjYXJyb3ctc2VxLTApIiBzdHJva2UtZGFzaGFycmF5PSIzIDMiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PC9wYXRoPjxyZWN0IHg9IjMxNi4yMCIgeT0iNTEyLjEwIiB3aWR0aD0iMTI2LjY1IiBoZWlnaHQ9Ijc2LjAwIiByeD0iMiIgcnk9IjIiIGZpbGw9IiNmZmZmZmYiIHN0cm9rZT0iI2U4ZThlOCIgY2xhc3M9ImVkZ2VMYWJlbCBzZXF1ZW5jZUVkZ2VMYWJlbCIgZGF0YS1lZGdlLWlkPSJlZGdlLTUiIGRhdGEtbGFiZWwta2luZD0iY2VudGVyIiBmaWxsLW9wYWNpdHk9IjAiIHN0cm9rZS1vcGFjaXR5PSIwIiBzdHJva2Utd2lkdGg9IjAuOCI+PC9yZWN0PjxnIGNsYXNzPSJlZGdlTGFiZWwiIGRhdGEtZWRnZS1pZD0iZWRnZS01IiBkYXRhLWxhYmVsLWtpbmQ9ImNlbnRlciI+PHRleHQgeD0iMzc5LjUyIiB5PSI1MzIuNjAiIGZpbGw9IiMxMTExMTEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJJbnRlcix1aS1zYW5zLXNlcmlmLHN5c3RlbS11aSwtYXBwbGUtc3lzdGVtLFNlZ29lIFVJLERlamFWdSBTYW5zLExpYmVyYXRpb24gU2FucyxzYW5zLXNlcmlmLE5vdG8gQ29sb3IgRW1vamksQXBwbGUgQ29sb3IgRW1vamksU2Vnb2UgVUkgRW1vamkiIGZvbnQtc2l6ZT0iMTQiPjx0c3BhbiB4PSIzNzkuNTIiIGR5PSIwLjAwIj5KU09OLWVuY29kZWQ8L3RzcGFuPjx0c3BhbiB4PSIzNzkuNTIiIGR5PSIyMS4wMCI+S290bGluIG1vZGVsPC90c3Bhbj48dHNwYW4geD0iMzc5LjUyIiBkeT0iMjEuMDAiPmNsYXNzPC90c3Bhbj48L3RleHQ+PC9nPjxwYXRoIGlkPSJlZGdlLTYiIGQ9Ik0gMjc1LjAwMCw2NjEuMjAwIEwgNzUuMDAwLDY2MS4yMDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzQ0NDQ0NCIgY2xhc3M9ImVkZ2VQYXRoIiBkYXRhLWVkZ2UtaWQ9ImVkZ2UtNiIgc3Ryb2tlLXdpZHRoPSIxLjUiIG1hcmtlci1lbmQ9InVybCgjYXJyb3ctc2VxLTApIiBzdHJva2UtZGFzaGFycmF5PSIzIDMiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PC9wYXRoPjxyZWN0IHg9IjExMS42NyIgeT0iNjA0LjAwIiB3aWR0aD0iMTI2LjY1IiBoZWlnaHQ9IjUyLjAwIiByeD0iMiIgcnk9IjIiIGZpbGw9IiNmZmZmZmYiIHN0cm9rZT0iI2U4ZThlOCIgY2xhc3M9ImVkZ2VMYWJlbCBzZXF1ZW5jZUVkZ2VMYWJlbCIgZGF0YS1lZGdlLWlkPSJlZGdlLTYiIGRhdGEtbGFiZWwta2luZD0iY2VudGVyIiBmaWxsLW9wYWNpdHk9IjAiIHN0cm9rZS1vcGFjaXR5PSIwIiBzdHJva2Utd2lkdGg9IjAuOCI+PC9yZWN0PjxnIGNsYXNzPSJlZGdlTGFiZWwiIGRhdGEtZWRnZS1pZD0iZWRnZS02IiBkYXRhLWxhYmVsLWtpbmQ9ImNlbnRlciI+PHRleHQgeD0iMTc1LjAwIiB5PSI2MjMuMDAiIGZpbGw9IiMxMTExMTEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJJbnRlcix1aS1zYW5zLXNlcmlmLHN5c3RlbS11aSwtYXBwbGUtc3lzdGVtLFNlZ29lIFVJLERlamFWdSBTYW5zLExpYmVyYXRpb24gU2FucyxzYW5zLXNlcmlmLE5vdG8gQ29sb3IgRW1vamksQXBwbGUgQ29sb3IgRW1vamksU2Vnb2UgVUkgRW1vamkiIGZvbnQtc2l6ZT0iMTQiPjx0c3BhbiB4PSIxNzUuMDAiIGR5PSIwLjAwIj5UeXBlZCBLb3RsaW48L3RzcGFuPjx0c3BhbiB4PSIxNzUuMDAiIGR5PSIyMS4wMCI+bW9kZWw8L3RzcGFuPjwvdGV4dD48L2c+PHJlY3QgeD0iMC4wMCIgeT0iMC4wMCIgd2lkdGg9IjE1MC4wMCIgaGVpZ2h0PSI2NS4wMCIgcng9IjMiIHJ5PSIzIiBmaWxsPSIjZjdmN2Y3IiBzdHJva2U9IiNlOGU4ZTgiIHN0cm9rZS13aWR0aD0iMS4wIj48L3JlY3Q+PHRleHQgeD0iNzUuMDAiIHk9IjM2LjAwIiBmaWxsPSIjMTExMTExIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSW50ZXIsdWktc2Fucy1zZXJpZixzeXN0ZW0tdWksLWFwcGxlLXN5c3RlbSxTZWdvZSBVSSxEZWphVnUgU2FucyxMaWJlcmF0aW9uIFNhbnMsc2Fucy1zZXJpZixOb3RvIENvbG9yIEVtb2ppLEFwcGxlIENvbG9yIEVtb2ppLFNlZ29lIFVJIEVtb2ppIiBmb250LXNpemU9IjE0Ij48dHNwYW4geD0iNzUuMDAiIGR5PSIwLjAwIj5BbmRyb2lkPC90c3Bhbj48L3RleHQ+PHJlY3QgeD0iNDA5LjA1IiB5PSIwLjAwIiB3aWR0aD0iMTUwLjAwIiBoZWlnaHQ9IjY1LjAwIiByeD0iMyIgcnk9IjMiIGZpbGw9IiNmN2Y3ZjciIHN0cm9rZT0iI2U4ZThlOCIgc3Ryb2tlLXdpZHRoPSIxLjAiPjwvcmVjdD48dGV4dCB4PSI0ODQuMDUiIHk9IjM2LjAwIiBmaWxsPSIjMTExMTExIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSW50ZXIsdWktc2Fucy1zZXJpZixzeXN0ZW0tdWksLWFwcGxlLXN5c3RlbSxTZWdvZSBVSSxEZWphVnUgU2FucyxMaWJlcmF0aW9uIFNhbnMsc2Fucy1zZXJpZixOb3RvIENvbG9yIEVtb2ppLEFwcGxlIENvbG9yIEVtb2ppLFNlZ29lIFVJIEVtb2ppIiBmb250LXNpemU9IjE0Ij48dHNwYW4geD0iNDg0LjA1IiBkeT0iMC4wMCI+SmF2YVNjcmlwdDwvdHNwYW4+PC90ZXh0PjxyZWN0IHg9IjYwOS4wNSIgeT0iMC4wMCIgd2lkdGg9IjE1MC4wMCIgaGVpZ2h0PSI2NS4wMCIgcng9IjMiIHJ5PSIzIiBmaWxsPSIjZjdmN2Y3IiBzdHJva2U9IiNlOGU4ZTgiIHN0cm9rZS13aWR0aD0iMS4wIj48L3JlY3Q+PHRleHQgeD0iNjg0LjA1IiB5PSIzNi4wMCIgZmlsbD0iIzExMTExMSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkludGVyLHVpLXNhbnMtc2VyaWYsc3lzdGVtLXVpLC1hcHBsZS1zeXN0ZW0sU2Vnb2UgVUksRGVqYVZ1IFNhbnMsTGliZXJhdGlvbiBTYW5zLHNhbnMtc2VyaWYsTm90byBDb2xvciBFbW9qaSxBcHBsZSBDb2xvciBFbW9qaSxTZWdvZSBVSSBFbW9qaSIgZm9udC1zaXplPSIxNCI+PHRzcGFuIHg9IjY4NC4wNSIgZHk9IjAuMDAiPlNlcnZlcjwvdHNwYW4+PC90ZXh0PjxyZWN0IHg9IjIwMC4wMCIgeT0iMC4wMCIgd2lkdGg9IjE1MC4wMCIgaGVpZ2h0PSI2NS4wMCIgcng9IjMiIHJ5PSIzIiBmaWxsPSIjZjdmN2Y3IiBzdHJva2U9IiNlOGU4ZTgiIHN0cm9rZS13aWR0aD0iMS4wIj48L3JlY3Q+PHRleHQgeD0iMjc1LjAwIiB5PSIzNi4wMCIgZmlsbD0iIzExMTExMSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkludGVyLHVpLXNhbnMtc2VyaWYsc3lzdGVtLXVpLC1hcHBsZS1zeXN0ZW0sU2Vnb2UgVUksRGVqYVZ1IFNhbnMsTGliZXJhdGlvbiBTYW5zLHNhbnMtc2VyaWYsTm90byBDb2xvciBFbW9qaSxBcHBsZSBDb2xvciBFbW9qaSxTZWdvZSBVSSBFbW9qaSIgZm9udC1zaXplPSIxNCI+PHRzcGFuIHg9IjI3NS4wMCIgZHk9IjAuMDAiPlppcGxpbmU8L3RzcGFuPjwvdGV4dD48cmVjdCB4PSIwLjAwIiB5PSI3MzIuNjAiIHdpZHRoPSIxNTAuMDAiIGhlaWdodD0iNjUuMDAiIHJ4PSIzIiByeT0iMyIgZmlsbD0iI2Y3ZjdmNyIgc3Ryb2tlPSIjZThlOGU4IiBzdHJva2Utd2lkdGg9IjEuMCI+PC9yZWN0Pjx0ZXh0IHg9Ijc1LjAwIiB5PSI3NjguNjAiIGZpbGw9IiMxMTExMTEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJJbnRlcix1aS1zYW5zLXNlcmlmLHN5c3RlbS11aSwtYXBwbGUtc3lzdGVtLFNlZ29lIFVJLERlamFWdSBTYW5zLExpYmVyYXRpb24gU2FucyxzYW5zLXNlcmlmLE5vdG8gQ29sb3IgRW1vamksQXBwbGUgQ29sb3IgRW1vamksU2Vnb2UgVUkgRW1vamkiIGZvbnQtc2l6ZT0iMTQiPjx0c3BhbiB4PSI3NS4wMCIgZHk9IjAuMDAiPkFuZHJvaWQ8L3RzcGFuPjwvdGV4dD48cmVjdCB4PSIyMDAuMDAiIHk9IjczMi42MCIgd2lkdGg9IjE1MC4wMCIgaGVpZ2h0PSI2NS4wMCIgcng9IjMiIHJ5PSIzIiBmaWxsPSIjZjdmN2Y3IiBzdHJva2U9IiNlOGU4ZTgiIHN0cm9rZS13aWR0aD0iMS4wIj48L3JlY3Q+PHRleHQgeD0iMjc1LjAwIiB5PSI3NjguNjAiIGZpbGw9IiMxMTExMTEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJJbnRlcix1aS1zYW5zLXNlcmlmLHN5c3RlbS11aSwtYXBwbGUtc3lzdGVtLFNlZ29lIFVJLERlamFWdSBTYW5zLExpYmVyYXRpb24gU2FucyxzYW5zLXNlcmlmLE5vdG8gQ29sb3IgRW1vamksQXBwbGUgQ29sb3IgRW1vamksU2Vnb2UgVUkgRW1vamkiIGZvbnQtc2l6ZT0iMTQiPjx0c3BhbiB4PSIyNzUuMDAiIGR5PSIwLjAwIj5aaXBsaW5lPC90c3Bhbj48L3RleHQ+PHJlY3QgeD0iNDA5LjA1IiB5PSI3MzIuNjAiIHdpZHRoPSIxNTAuMDAiIGhlaWdodD0iNjUuMDAiIHJ4PSIzIiByeT0iMyIgZmlsbD0iI2Y3ZjdmNyIgc3Ryb2tlPSIjZThlOGU4IiBzdHJva2Utd2lkdGg9IjEuMCI+PC9yZWN0Pjx0ZXh0IHg9IjQ4NC4wNSIgeT0iNzY4LjYwIiBmaWxsPSIjMTExMTExIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSW50ZXIsdWktc2Fucy1zZXJpZixzeXN0ZW0tdWksLWFwcGxlLXN5c3RlbSxTZWdvZSBVSSxEZWphVnUgU2FucyxMaWJlcmF0aW9uIFNhbnMsc2Fucy1zZXJpZixOb3RvIENvbG9yIEVtb2ppLEFwcGxlIENvbG9yIEVtb2ppLFNlZ29lIFVJIEVtb2ppIiBmb250LXNpemU9IjE0Ij48dHNwYW4geD0iNDg0LjA1IiBkeT0iMC4wMCI+SmF2YVNjcmlwdDwvdHNwYW4+PC90ZXh0PjxyZWN0IHg9IjYwOS4wNSIgeT0iNzMyLjYwIiB3aWR0aD0iMTUwLjAwIiBoZWlnaHQ9IjY1LjAwIiByeD0iMyIgcnk9IjMiIGZpbGw9IiNmN2Y3ZjciIHN0cm9rZT0iI2U4ZThlOCIgc3Ryb2tlLXdpZHRoPSIxLjAiPjwvcmVjdD48dGV4dCB4PSI2ODQuMDUiIHk9Ijc2OC42MCIgZmlsbD0iIzExMTExMSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkludGVyLHVpLXNhbnMtc2VyaWYsc3lzdGVtLXVpLC1hcHBsZS1zeXN0ZW0sU2Vnb2UgVUksRGVqYVZ1IFNhbnMsTGliZXJhdGlvbiBTYW5zLHNhbnMtc2VyaWYsTm90byBDb2xvciBFbW9qaSxBcHBsZSBDb2xvciBFbW9qaSxTZWdvZSBVSSBFbW9qaSIgZm9udC1zaXplPSIxNCI+PHRzcGFuIHg9IjY4NC4wNSIgZHk9IjAuMDAiPlNlcnZlcjwvdHNwYW4+PC90ZXh0PjxzdHlsZT4KICAuYWN0b3IsCiAgLmxhYmVsQm94LAogIC5ub2RlIHJlY3QsCiAgLm5vZGUgcG9seWdvbiwKICAubm9kZSBjaXJjbGUsCiAgLmNsdXN0ZXIgcmVjdCwKICAuc2VjdGlvbiwKICAudGFzaywKICAudGFza1RleHRPdXRzaWRlUmlnaHQsCiAgLnRhc2tUZXh0T3V0c2lkZUxlZnQgewogICAgZmlsbDogI2Y3ZjdmNyAhaW1wb3J0YW50OwogICAgc3Ryb2tlOiAjZThlOGU4ICFpbXBvcnRhbnQ7CiAgfQoKICAubm90ZSB7CiAgICBmaWxsOiAjZTBjOGRiICFpbXBvcnRhbnQ7CiAgICBzdHJva2U6ICM3YTNkNmUgIWltcG9ydGFudDsKICB9CgogIC5hY3Rvci1saW5lLAogIC5tZXNzYWdlTGluZTAsCiAgLm1lc3NhZ2VMaW5lMSwKICAubG9vcExpbmUsCiAgLmVkZ2VQYXRoIC5wYXRoLAogIC5mbG93Y2hhcnQtbGluaywKICAubWFya2VyLAogIC5tYXJrZXIgcGF0aCwKICBbaWQkPSItYXJyb3doZWFkIl0gcGF0aCwKICBbaWQkPSItZmlsbGVkLWhlYWQiXSBwYXRoLAogIFtpZCQ9Ii1jcm9zc2hlYWQiXSBwYXRoLAogIFtpZCQ9Ii1zdGlja1RvcEFycm93SGVhZCJdIHBhdGgsCiAgW2lkJD0iLXN0aWNrQm90dG9tQXJyb3dIZWFkIl0gcGF0aCwKICBbaWQkPSItc29saWRUb3BBcnJvd0hlYWQiXSBwYXRoLAogIFtpZCQ9Ii1zb2xpZEJvdHRvbUFycm93SGVhZCJdIHBhdGggewogICAgc3Ryb2tlOiAjNDQ0NDQ0ICFpbXBvcnRhbnQ7CiAgICBmaWxsOiAjNDQ0NDQ0ICFpbXBvcnRhbnQ7CiAgfQoKICAubWVzc2FnZVRleHQsCiAgLmxhYmVsVGV4dCwKICAubGFiZWxUZXh0IHRzcGFuLAogIC5sb29wVGV4dCwKICAubG9vcFRleHQgdHNwYW4sCiAgLm5vdGVUZXh0LAogIC5ub3RlVGV4dCB0c3BhbiwKICAuYWN0b3ItYm94LAogIC5hY3Rvci1ib3ggdHNwYW4sCiAgLnNlY3Rpb25UaXRsZSwKICAuc2VjdGlvblRpdGxlIHRzcGFuLAogIHRleHQsCiAgdHNwYW4gewogICAgZmlsbDogIzExMTExMSAhaW1wb3J0YW50OwogICAgc3Ryb2tlOiBub25lICFpbXBvcnRhbnQ7CiAgfQoKICAuYWN0aXZhdGlvbjAsCiAgLmFjdGl2YXRpb24xLAogIC5hY3RpdmF0aW9uMiwKICAuc2VxdWVuY2VOdW1iZXIgewogICAgZmlsbDogI2ZmZmZmZiAhaW1wb3J0YW50OwogICAgc3Ryb2tlOiAjZThlOGU4ICFpbXBvcnRhbnQ7CiAgfQo8L3N0eWxlPjwvc3ZnPg==\" alt=\"Mermaid diagram\" loading=\"lazy\" decoding=\"async\">\n<p>Step 1: Converted the data model layer to Kotlin Multiplatform, <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/22f4e542e4ac820be152a16bda2bb650588d7b24\">which went relatively fine</a>.</p>\n<p>Step 2: Added the <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/c1b35eb2019799547b53fd8fa4aecfab1b18e11c\">actual Zipline-backed parsers</a>. I replaced <a href=\"https://github.com/fleeksoft/Kspoon\">Kspoon</a> with the underlying <a href=\"https://github.com/fleeksoft/Ksoup\">Ksoup</a> library and called it directly instead of through a <code>kotlinx.serialization</code> based parser.</p>\n<p>Step 3: Migrated the Android code to using <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/a015794e1f7c3eabad8a965c4a991b13de0c9be6\">the new Zipline-backed parser</a>. I’ve put together a <a href=\"https://github.com/msfjarvis/compose-lobsters/blob/a015794e1f7c3eabad8a965c4a991b13de0c9be6/api/src/main/kotlin/dev/msfjarvis/claw/api/converters/ZiplineHtmlConverterFactory.kt\">terrible kludge</a> to determine when to call which parser method, and I am sure there’s a nicer way to do this that evaded me at the time.</p>\n<p>Step 4: Embedded the latest copy of the Zipline assets <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/3658aafd619cb36997503d0a408571d322531fa4\">automatically in the APK</a> to speed up the first-launch experience. Since my app really only works with the internet it’s not a dealbreaker if I skip this, but I felt it’s better to have a fallback in case Cloudflare or I have done something to cause my remote manifest to become inaccessible. I later discovered this logic wasn’t actually falling back to embedded assets like I assumed, which I <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/2c9fc7e5dc8f210157beaeef0661f11a906160e0\">fixed</a> by explicitly loading the embedded assets manually in case of errors.</p>\n<p>And that should’ve been it! Except…</p>\n<h2 id=\"the-troubles-begin\">The troubles begin</h2>\n<p>After the mechanical refactors I tried running the app and immediately ran into <code>stack overflow</code> errors. Zipline helpfully includes a <a href=\"https://github.com/cashapp/zipline/tree/4fa57be9c61bf2b484a380e23d07e99e9b18f04f/zipline-profiler\">sampling profiler</a> to identify bottlenecks, but I wasn’t able to figure out how to make it help with this specific problem. I also didn’t exactly understand what was causing this stack overflow. I flailed around for a fair bit but that’s not really interesting, so here’s a quick summary.</p>\n<table>\n<thead>\n<tr>\n<th>Thing tried</th>\n<th>Impact</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><a href=\"https://github.com/msfjarvis/compose-lobsters/commit/344da065918d09e314ddba4bd8eb8e77bd27a5c2\">Cache Regex objects and replace functional chains with simple loops</a></td>\n<td>Nothing whatsoever</td>\n</tr>\n<tr>\n<td>Simplifying parser models to drop generated serializable code <small><a href=\"https://github.com/msfjarvis/compose-lobsters/commit/b2adb282756a6cb02ae20ea8a70fde0b947c6338\">[1]</a><a href=\"https://github.com/msfjarvis/compose-lobsters/commit/fbe6f65e74c978198f1bd91a87db1b67faa3b6b8\">[2]</a><a href=\"https://github.com/msfjarvis/compose-lobsters/commit/fbc0201c1060a7d175f59b4ba034053d798d848a\">[3]</a></small></td>\n<td>Made the code more annoying to deal with</td>\n</tr>\n<tr>\n<td><a href=\"https://github.com/msfjarvis/compose-lobsters/commit/8d122636fd023a6596d338ae33760e2cd780037b\">Replaced recursive code with a simpler loop</a></td>\n<td>Native crash instead of stack overflow!</td>\n</tr>\n<tr>\n<td><a href=\"https://github.com/msfjarvis/compose-lobsters/commit/c2fd16939bbee04688d248941f8d2c8a7e1b69e4\">Lazy initialization</a></td>\n<td>Nothing</td>\n</tr>\n<tr>\n<td><a href=\"https://github.com/msfjarvis/compose-lobsters/commit/a8e9a5bf0f50c21a737adcdfa6e994b9bda53db8\">De-duplicating fields</a></td>\n<td>Nothing</td>\n</tr>\n<tr>\n<td><a href=\"https://github.com/msfjarvis/compose-lobsters/commit/c78a003cb9bb7ecf88e7e65cf8335dd6be3e1112\">Removing data classes</a></td>\n<td>Nothing</td>\n</tr>\n</tbody>\n</table>\n<p>At this point it had already been almost two weeks, and I was pretty tired of going from one confusing problem to another and gave up on this adventure for a few days. After having some success at work with giving GPT-5.5 a harness based on the <a href=\"https://developer.android.com/tools/agents/android-cli\"><code>android</code> CLI</a> to let it debug a complex issue by itself, I decided to bring that hammer back to this problem. I wrote out a handoff doc, cloned a local copy of Zipline for the clanker to refer to, and let it attack the problem overnight.</p>\n<p>In the morning it claimed to have solved the crash, which I was able to verify myself. However, as is often the case with LLMs, the code was completely inscrutable. I asked it to commit the changes with a proper commit message but <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/829a6fe4716a20f53560c61e0220eb83e7746b6f\">that shed no real light</a>. There were <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/829a6fe4716a20f53560c61e0220eb83e7746b6f#diff-e0524b31f8f252d91067b8d2101bca80c827aa9282036342bc3a2b914a51acb7\">tests</a>, but it was clear the clanker was too lazy and had chosen to invent a separate code path just for tests so it could get away with writing mostly useless fluff. I <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/ed9ba64162875e61ae42b352922e68c8f2a7273d\">deleted the tests</a> and decided I’d add some manually written ones when I understood the code.</p>\n<p>GPT-5.5 had also found another source of problems, the stack size. It <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/4539e0a6b9c13d583e3970044e480457636f5945\">raised the stack size limit</a> from the default 6 MB, and changed the code to ensure every Zipline operation ran <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/13a9d72c35b679da523a436554f39f2e27d01b58\">on the same thread/dispatcher</a> that we initialized Zipline with. I had to go look that up, and apparently the bigger stack size is an <a href=\"https://github.com/cashapp/zipline/blob/4fa57be9c61bf2b484a380e23d07e99e9b18f04f/zipline/src/hostMain/kotlin/app/cash/zipline/Zipline.kt#L199-L202\">upstream recommendation</a> buried in a code comment. Also, TIL that <code>Thread</code> has a <a href=\"https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#Thread-java.lang.ThreadGroup-java.lang.Runnable-java.lang.String-long-\">four-argument constructor</a>!</p>\n<p>I immediately understood the reason for the increased stack size, but it took a bit to realize that since JavaScript is single-threaded, it only makes sense we confine our reads and writes of JavaScript-exposed memory to that same thread.</p>\n<p>Once I verified everything was working, I shipped it to the internal testing track.</p>\n<h3 id=\"that-bit-of-code\"><em>That</em> bit of code</h3>\n<div class=\"markdown-alert markdown-alert-note\" dir=\"auto\">\n<p class=\"markdown-alert-title\" dir=\"auto\"><svg class=\"octicon\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>NOTE</p>\n<p>This serializer was ultimately unnecessary and no longer exists. I’m including it because understanding and then removing it was an important part of validating the LLM-generated changes.</p>\n</div>\n<p>While I did merge the branch without really understanding what the <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/829a6fe4716a20f53560c61e0220eb83e7746b6f\">ParserSerializers code</a> was doing, I was determined to figure it out. To improve my understanding of it, I talked my way through the code to <a href=\"https://github.com/mayankofficial999\">Mayank</a> at work, and eventually put in some <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/7740177b86e4630b5a48753d18047a4bf5e7ce71\">handwritten tests</a> to validate my mental model.</p>\n<p>The clanker invented a very rudimentary serialization format that creates a flat string that is smaller than a comparative JSON object but trades that with forcing you to know ahead of time what field names to expect and in what order.</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"kotlin\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FE8929\">private</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">class</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">PacketWriter</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FE8929\">private</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">val</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> parts </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">StringBuilder</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">fun</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">string</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">String</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">): </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">PacketWriter</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">parts.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">append</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.length).</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">append</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">':'</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">).</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">append</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">return</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--1:#91AFA3\">this</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Other types skipped for brevity</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">fun</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">build</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(): </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">String</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> parts.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">toString</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// To go from object to String, you call methods on</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// the PacketWriter builder to feed in the fields of your values.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">fun</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">encode</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">Tag</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">): </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">String</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">return</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">PacketWriter</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">string</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.tag)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">string</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.description)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">boolean</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.privileged)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">boolean</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.active)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">string</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.category)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">boolean</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.isMedia)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">double</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.hotnessMod)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">build</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"private class PacketWriter {  private val parts = StringBuilder()  fun string(value: String): PacketWriter {    parts.append(value.length).append(':').append(value)    return this  }  // Other types skipped for brevity  fun build(): String = parts.toString()}// To go from object to String, you call methods on// the PacketWriter builder to feed in the fields of your values.fun encode(value: Tag): String {  return PacketWriter()    .string(value.tag)    .string(value.description)    .boolean(value.privileged)    .boolean(value.active)    .string(value.category)    .boolean(value.isMedia)    .double(value.hotnessMod)    .build()}\"><div></div></button></div></figure></div>\n<p>Effectively, what you’re seeing is a classic <a href=\"https://textbooks.cs.ksu.edu/cc210/05-loops/07-java/07-accumulator/\">accumulator pattern</a> but in a String form. Instead of building up a JSON object, we construct a “flat” string that is essentially a concatenation of many <code>len(content):content</code> pairs. Null values are encoded with a length of -1; the colon is retained to keep parsing unambiguous.</p>\n<p>The deserialization code will be self-explanatory if you were able to follow the serializer.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"kotlin\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FE8929\">private</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">class</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">PacketReader</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FE8929\">private</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">val</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> payload: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">String</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FE8929\">private</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">var</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> index </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">0</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// This code has to be more defensive about invalid</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// inputs, and hence most of the method is just validation.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">fun</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">string</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(): </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">String</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">val</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> separatorIndex </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> payload.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">indexOf</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">':'</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, index)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">if</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (separatorIndex </span><span style=\"--0:#116B70;--1:#8EC07C\">==</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">-</span><span style=\"--0:#A54107;--1:#D995A8\">1</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) </span><span style=\"--0:#7B34D9;--1:#FC8678\">throw</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">SerializationException</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"Malformed payload\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">val</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> length </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> payload.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">substring</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(index, separatorIndex).</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">toInt</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">index </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> separatorIndex </span><span style=\"--0:#116B70;--1:#8EC07C\">+</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">1</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">if</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (length </span><span style=\"--0:#116B70;--1:#8EC07C\">&lt;</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">0</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) </span><span style=\"--0:#7B34D9;--1:#FC8678\">throw</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">SerializationException</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"Expected non-null string\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">val</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> endIndex </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> index </span><span style=\"--0:#116B70;--1:#8EC07C\">+</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> length</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">if</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (endIndex </span><span style=\"--0:#116B70;--1:#8EC07C\">&gt;</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> payload.length) </span><span style=\"--0:#7B34D9;--1:#FC8678\">throw</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">SerializationException</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"Malformed payload\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">return</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> payload.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">substring</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(index, endIndex).</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">also</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> { index </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> endIndex }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Other types skipped for brevity</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Due to the nature of this scheme, deserialization is sensitive</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// to the order in which serialization occurred. Your field ordering</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// was already public API for public types, it is now also required</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// to be stable for this serialization codec.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">fun</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">decodeObject</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(payload: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">String</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">): </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">Tag</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">val</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> reader </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">PacketReader</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(payload)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">return</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">Tag</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">tag </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> reader.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">string</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(),</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">description </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> reader.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">string</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(),</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">privileged </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> reader.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">boolean</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(),</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">active </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> reader.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">boolean</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(),</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">category </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> reader.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">string</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(),</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">isMedia </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> reader.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">boolean</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(),</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">hotnessMod </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> reader.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">double</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(),</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"private class PacketReader(private val payload: String) {  private var index = 0  // This code has to be more defensive about invalid  // inputs, and hence most of the method is just validation.  fun string(): String {    val separatorIndex = payload.indexOf(':', index)    if (separatorIndex == -1) throw SerializationException(&quot;Malformed payload&quot;)    val length = payload.substring(index, separatorIndex).toInt()    index = separatorIndex + 1    if (length < 0) throw SerializationException(&quot;Expected non-null string&quot;)    val endIndex = index + length    if (endIndex > payload.length) throw SerializationException(&quot;Malformed payload&quot;)    return payload.substring(index, endIndex).also { index = endIndex }  }  // Other types skipped for brevity}// Due to the nature of this scheme, deserialization is sensitive// to the order in which serialization occurred. Your field ordering// was already public API for public types, it is now also required// to be stable for this serialization codec.fun decodeObject(payload: String): Tag {  val reader = PacketReader(payload)  return Tag(    tag = reader.string(),    description = reader.string(),    privileged = reader.boolean(),    active = reader.boolean(),    category = reader.string(),    isMedia = reader.boolean(),    hotnessMod = reader.double(),  )}\"><div></div></button></div></figure></div>\n<p>To decode individual fields, we hold a top-level <code>index</code> variable and slice out a substring between <code>index</code> and the first <code>:</code> character to determine the length of data to be read. Then we read off that much after the colon, and move <code>index</code> to the first unread position. This is repeated till the decoder has read all fields it wants, and the object is constructed.</p>\n<p>This is obviously extremely naive: The decoder reads the fields it expects; it does not ensure that the entire payload was traversed or reject trailing data. Invalid booleans become false, any negative nullable length becomes null, and numeric conversion exceptions are not normalized.</p>\n<h2 id=\"deployment\">Deployment</h2>\n<p>Once I had the code working, the remaining part was deployment. I didn’t want to make this complicated or rely on GitHub Pages as a CDN which they discourage, so I opted to host it myself. The directory layout I chose is intentionally quite simplified and idiot-proofed against yours truly, who did fuck it up many times. The directory structure is hopefully self-explanatory:</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plaintext\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">/var/lib/claw-deploy</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">├── current -&gt; /var/lib/claw-deploy/releases/20260712T113110Z</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">└── releases</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">├── 20260710T065949Z</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">│&nbsp;&nbsp; ├── Claw-zipline-parser.zipline</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">│&nbsp;&nbsp; ├── fleeksoft-io-charset.zipline</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">│&nbsp;&nbsp; ├── fleeksoft-io-io-core.zipline</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">│&nbsp;&nbsp; ├── kotlin-kotlin-stdlib.zipline</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">│&nbsp;&nbsp; ├── kotlin_org_jetbrains_kotlin_kotlin_dom_api_compat.zipline</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">│&nbsp;&nbsp; ├── kotlinx-atomicfu.zipline</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">│&nbsp;&nbsp; ├── kotlinx-coroutines-core.zipline</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">│&nbsp;&nbsp; ├── kotlinx-serialization-kotlinx-serialization-core.zipline</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">│&nbsp;&nbsp; ├── kotlinx-serialization-kotlinx-serialization-json.zipline</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">│&nbsp;&nbsp; ├── ksoup.zipline</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">│&nbsp;&nbsp; ├── manifest.zipline.json</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">│&nbsp;&nbsp; ├── Stately-stately-concurrency.zipline</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">│&nbsp;&nbsp; ├── Stately-stately-strict.zipline</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">│&nbsp;&nbsp; └── zipline-root-zipline.zipline</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">└── 20260712T113110Z</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">...</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"/var/lib/claw-deploy├── current -> /var/lib/claw-deploy/releases/20260712T113110Z└── releases    ├── 20260710T065949Z    │&nbsp;&nbsp; ├── Claw-zipline-parser.zipline    │&nbsp;&nbsp; ├── fleeksoft-io-charset.zipline    │&nbsp;&nbsp; ├── fleeksoft-io-io-core.zipline    │&nbsp;&nbsp; ├── kotlin-kotlin-stdlib.zipline    │&nbsp;&nbsp; ├── kotlin_org_jetbrains_kotlin_kotlin_dom_api_compat.zipline    │&nbsp;&nbsp; ├── kotlinx-atomicfu.zipline    │&nbsp;&nbsp; ├── kotlinx-coroutines-core.zipline    │&nbsp;&nbsp; ├── kotlinx-serialization-kotlinx-serialization-core.zipline    │&nbsp;&nbsp; ├── kotlinx-serialization-kotlinx-serialization-json.zipline    │&nbsp;&nbsp; ├── ksoup.zipline    │&nbsp;&nbsp; ├── manifest.zipline.json    │&nbsp;&nbsp; ├── Stately-stately-concurrency.zipline    │&nbsp;&nbsp; ├── Stately-stately-strict.zipline    │&nbsp;&nbsp; └── zipline-root-zipline.zipline    └── 20260712T113110Z    ...\"><div></div></button></div></figure></div>\n<p><code>current</code> is a symlink to a timestamped directory containing the compiled Zipline artifacts. The size of individual releases is relatively tiny so I keep all previous releases around, making it easy to roll back during one of those aforementioned fuck ups. The app only ever fetches the <code>/current</code> path, so to the client this bundle is effectively unversioned.</p>\n<p>The documented trust model for Zipline states that the host app and the build server are implicitly trusted, so I stuck with the official path of signing the manifest with an Ed25519 key. The app fetches the manifest, validates it is signed with the trusted key that is embedded in the app, then downloads the modules listed in it. Debug builds do not fetch remote assets, only loading the embedded bundle to ease testing. Failing to fetch the remote manifest also gracefully degrades to loading the embedded copy, which means 100% uptime is not a strict requirement. The K/JS bundle is currently only the parser code, but even more functionality could end up in there at some point. Exciting stuff! Unless…</p>\n<p>When I went to make my first over-the-air release, I ran into caching problems :) While Zipline takes care of disabling the HTTP client cache when <a href=\"https://github.com/cashapp/zipline/blob/4fa57be9c61bf2b484a380e23d07e99e9b18f04f/zipline-loader/src/commonMain/kotlin/app/cash/zipline/loader/internal/fetcher/HttpFetcher.kt#L150\">downloading <code>.zipline</code> files</a>, it doesn’t do so for <a href=\"https://github.com/cashapp/zipline/blob/4fa57be9c61bf2b484a380e23d07e99e9b18f04f/zipline-loader/src/commonMain/kotlin/app/cash/zipline/loader/internal/fetcher/HttpFetcher.kt#L142\"><code>manifest.zipline.json</code></a>. Thus, a server with caching enabled can cause OkHttp to load its cached and outdated manifest, which results in new <code>.zipline</code> modules being downloaded without an updated manifest, ultimately causing checksum verification to fail. This was a failure state I wasn’t aware of when I first implemented Zipline in the app, and had to patch it on both ends: setting the right <a href=\"https://github.com/msfjarvis/dotfiles/commit/f6bb04b5e67005c04afe64bef20cd66fb2524d06\">Cache-Control headers on the server</a>, and deleting the <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/a1940881850b4020199667e0b342ac15e6880b2b\">manifest URL cache entry in the app</a>. That seems to have done the job, and I have been able to roll out two updates since making these changes without any problems.</p>\n<h2 id=\"what-changes-for-users\">What changes for users</h2>\n<p>Since I didn’t want to involve any complex server-side logic, my deployment setup is extremely rudimentary which makes some aspects of the in-app behavior suboptimal. The way things are configured right now, the app re-fetches the manifest on each launch, which slows down the initial content load. Every app launch sends at least one network request to my server, which means I was now accidentally collecting user IPs. To mitigate that on the origin server, I have disabled <a href=\"https://github.com/msfjarvis/dotfiles/commit/c5d73fc7d8f652bd3eedd3f9953e81bce1095201\">logging in my webserver</a> and deleted the logs that were already recorded.</p>\n<p>The release bundles are also unversioned, which means I do not yet know how I would roll out a breaking change, which I’ve already made as you’ll read in the very next section. This is all very new and exciting to figure out, even if it’s making my app somewhat worse in the short term.</p>\n<p>Google Play’s <a href=\"https://support.google.com/googleplay/android-developer/answer/16559646?hl=en\">policy on remote code loading</a> exempts code that is run in interpreters, which means the Zipline approach is compliant.</p>\n<h2 id=\"on-debugging-with-llms\">On debugging with LLMs</h2>\n<p>I generally take pride in being meticulous and having a good system-level understanding of the code I work on, but here I gave up and deferred to an LLM which felt like admitting defeat. When it solved the problem overnight in a problem space I barely understood I was pretty glad in the moment to be done with it, but eventually that made way for just disappointment. I’d have probably felt better if I didn’t give up at that point and just continued investigating and researching, after all that was the entire reason Claw even exists. I’ve used it for <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/29c374859b17c5fcef03585b8a01c00070de9097\">nearly 5 years</a> to learn new things the hard way and I gave up on that this time just because I had a clanker handy.</p>\n<p>The real kicker? All that inscrutable code is also completely useless. The stack size change and confining operations to the shared thread were the actual fixes. As I mentioned at the start, as part of writing this post over the past month I reviewed and validated every bit of that PR again which surfaced these hallucinations. As of writing, the <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/194ccb540abf74b9ab499bea4a8f94e8725d62ac\">custom serializer is gone</a>, and so is the <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/7e53354a14e72f73a4d17fe28f68bfd06ec52155\">zipline-parser-api module</a>. Removing the custom serializer and its model classes is a breaking API change, and my internal testing track <a href=\"https://github.com/msfjarvis/compose-lobsters/issues/1237\">is now broken</a> because I haven’t updated the remote bundle with these changes. Another thing to figure out, sans LLMs this time :)</p>\n<p>I’ll try harder to hold off on the LLMs going forward. Massive thanks to <a href=\"https://publicobject.com/\">Jesse Wilson</a> whose writing and public speaking have helped me rediscover the fun of programming. Go check out his talk <a href=\"https://publicobject.com/2026/05/22/deconstructing-okhttp/\">Deconstructing OkHttp</a>, it’s a great use of your time even if you do not know or care about OkHttp! You’ll learn a good deal about evolving a complex library over a period of years and how the benefits of thoughtful API design stack up.</p>\n<h2 id=\"closing-notes\">Closing notes</h2>\n<p>The effort both taught me more about Zipline and solved a problem I fully expected to encounter. I was able to roll <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/5c37772226eeba47e703074f3145ca8cb4ec1f95\">out a tag parsing fix</a> via Zipline before publishing it to Google Play. I then <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/4d02c0e29f10e48a7cbd191b5853ce0a54d29780\">extended the fix to search</a>, and <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/1a201a5430ec5d19d467bdd045c403e420e17c56\">added OP markers to submitters</a> — things I missed before the release.</p>\n<p>All in all, this has been quite the learning experience and I’m excited to once again have fun while programming.</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/apologies-for-the-rss-mishap/",
      "url": "https://msfjarvis.dev/posts/apologies-for-the-rss-mishap/",
      "title": "Apologies for the RSS mishap!",
      "date_published": "2026-07-10T19:34:00.000Z",
      "summary": "Defeated yet again by an entirely too complicated build pipeline",
      "content_html": "<p>Just a quick apology to all subscribers of the RSS feed, whom I accidentally sent a <em>lot</em> of duplicate posts to.</p>\n<p>I was working on the site and pushed to the wrong deployment, which caused the main site to start behaving like it was the preview site which includes my draft posts. Lots of duplicates and incomplete posts have now been dumped into your feed readers due to this, since it took me over an hour to figure out I had the mistake.</p>\n<p>Your best course of action is to just delete and re-add the feeds instead of trying to hunt down the duplicates.</p>\n<p>Sorry again!</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/julyreply-re-stuck-in-draft/",
      "url": "https://msfjarvis.dev/posts/julyreply-re-stuck-in-draft/",
      "title": "JulyReply - Re: Stuck in draft",
      "date_published": "2026-07-09T18:52:00.000Z",
      "summary": "On drafts and being okay with them",
      "content_html": "<div class=\"markdown-alert markdown-alert-note\" dir=\"auto\">\n<p class=\"markdown-alert-title\" dir=\"auto\"><svg class=\"octicon\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>NOTE</p>\n<p>This post is part of the <a href=\"https://robertbirming.com/julyreply-2026-blog-connecting/\">JulyReply</a> blogging event</p>\n</div>\n<p>Gordon wrote about his blog posts being <a href=\"https://www.gordonmclean.co.uk/2026/07/07/stuck-in-draft-3/\">stuck in draft</a>, which is a problem I also have to some extent – my drafts site has 75 posts right now compared to the 67 on the main site. At least one of those drafts carries a date from 2020!</p>\n<p>I agree with him on <em>urgency</em> being a strong motivator. I’ve turned around similarly sized posts in under an hour and also in <strong>months</strong>. My interest in specific things ebbs and flows quite frequently, and often it will be a very long time before I can summon the requisite curiosity and motivation to wrap up a draft. Many times, it will be someone chancing across that draft and <a href=\"https://github.com/msfjarvis/msfjarvis.dev/issues/79\">posting some feedback</a> which motivates me to finish it off. Other times I’ll simply post about having an incomplete draft and let someone express interest, which then becomes my driving force to deliver that bit of writing.</p>\n<p>And yet, in MOST cases, I just no longer care about the topic to finish that draft ¯\\<em>(ツ)</em>/¯</p>\n<p>I like having ideas written down <em>anywhere</em>, so my blogging process will almost always start with writing down either broad bullet points or a stream of consciousness dump into a draft. Most of these drafts will become something real in a few weeks time, and some of them will sink to the bottom to be forgotten forever. I think I’m okay with that.</p>\n<p>That said, I’ll take a page out of Gordon’s metaphorical book and also list some of my (comparatively uninteresting) drafts that I would like to finish. Reach out on <a href=\"https://fantastic.earth/@msfjarvis\">the Fediverse</a>, via <a href=\"mailto:me@msfjarvis.dev\">email</a>, or whatever communication channel you can grab a hold of me through and let me know what looks interesting. I do apologize that this is all technical content since I’m still learning to be a human on this blog outside my weeknotes 😅</p>\n<p>I’ll do a count on 17th July, which will give me around 2 weeks to finish up the post.</p>\n<ul>\n<li>Using <a href=\"https://github.com/square/retrofit\">Retrofit</a> to disguise scraping as a REST API</li>\n<li>Rewriting the <a href=\"https://github.com/msfjarvis/compose-lobsters\">Claw</a> HTML parser for over the air updates (PSA: LLMs were used for debugging)</li>\n<li>How I use Git</li>\n<li>Using Docker containers in NixOS</li>\n<li>Building a Webmentions server on top of the Cloudflare Developer Platform (PSA: employer shilling)</li>\n<li>Preview-driven Compose UI development with LLMs and the android CLI</li>\n</ul>\n<p>P.S. <a href=\"https://abhinavsarkar.net\">Abhinav</a> built a <a href=\"https://abhinavsarkar.net/upcoming/\">cool page</a> on his site serving this exact purpose, go cast your vote there too if you enjoy quality technical writing! I wanted to steal the idea and build a voting UI but that would make <em>this</em> post get stuck in draft!</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/my-first-spam-webmention/",
      "url": "https://msfjarvis.dev/posts/my-first-spam-webmention/",
      "title": "My first spam Webmention",
      "date_published": "2026-07-07T10:39:00.000Z",
      "summary": "Finally being forced to build moderation capabilities into my Webmentions server",
      "content_html": "<p>I finally got my first spam Webmention on this site</p>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/bsky-spam-post-light.CztkskgE.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/bsky-spam-post-light.CztkskgE.webp\" data-pswp-dark-src=\"https://msfjarvis.dev/_astro/bsky-spam-post-dark.D9UhSLjg.webp\" data-pswp-width=\"920\" data-pswp-height=\"167\" data-pswp-caption=\"A spam post from Bluesky that bridged over as a Webmention\" aria-label=\"Screenshot of a Bluesky spam post asking the reader to visit a pinned page.\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><source media=\"(prefers-color-scheme: dark)\" srcset=\"/_astro/bsky-spam-post-dark.D9UhSLjg.webp\" data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/bsky-spam-post-light.CztkskgE.webp\" alt=\"Screenshot of a Bluesky spam post asking the reader to visit a pinned page.\" loading=\"lazy\" decoding=\"async\" width=\"920\" height=\"167\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">A spam post from Bluesky that bridged over as a Webmention</figcaption></figure>\n<p>I chanced across this on one of my weeknotes, which seems to have already been deleted by Bluesky moderation or the poster by the time I got around to seeing it. I have been coasting along for a few months now without any moderation set up to easily moderate Webmentions, but I clearly should be paying more attention. This is far from being bad as far as spam goes, but I’d like to be prepared for the eventuality of something really terrible showing up in one of these.</p>\n<p>So far I had been resorting to manually running queries in the Cloudflare dashboard’s <a href=\"https://developers.cloudflare.com/d1/get-started/#4-run-a-query-against-your-d1-database\">D1 SQL Explorer</a>, but that’s too slow and complex to serve as an actual moderation tool.</p>\n<p>To that effect I now have a very brutalist dashboard being served by the Webmentions server (code <a href=\"https://git.msfjarvis.dev/msfjarvis/acceptable-vibes/src/branch/main/webmentions-server\">here</a>) which has a list of all pages that have received Webmentions. To mask my inability to do good web design, it is styled using the <a href=\"https://oat.ink\">Oat</a> CSS library like all my non-blog web projects. I find the minimalist aesthetic of Oat quite nice and it rarely needs any significant changes to a semantically laid out page when you add it.</p>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/pages-with-webmentions-light.C1cp2rAb.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/pages-with-webmentions-light.C1cp2rAb.webp\" data-pswp-dark-src=\"https://msfjarvis.dev/_astro/pages-with-webmentions-dark.DDGPeCD9.webp\" data-pswp-width=\"1280\" data-pswp-height=\"1392\" aria-label=\"Webmentions admin page listing site pages with mention counts and verification times.\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><source media=\"(prefers-color-scheme: dark)\" srcset=\"/_astro/pages-with-webmentions-dark.DDGPeCD9.webp\" data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/pages-with-webmentions-light.C1cp2rAb.webp\" alt=\"Webmentions admin page listing site pages with mention counts and verification times.\" loading=\"lazy\" decoding=\"async\" width=\"1280\" height=\"1392\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a></figure>\n<p>Clicking on one of these links takes you to this equally terrible UI which could be reduced down to a list with delete buttons and still serve exactly the same purpose. Not exactly rocket science here.</p>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/received-webmentions-light.B3qD9kGF.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/received-webmentions-light.B3qD9kGF.webp\" data-pswp-dark-src=\"https://msfjarvis.dev/_astro/received-webmentions-dark.CCKpzs_9.webp\" data-pswp-width=\"1248\" data-pswp-height=\"2301\" aria-label=\"Admin page showing received webmentions for one post with delete buttons.\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><source media=\"(prefers-color-scheme: dark)\" srcset=\"/_astro/received-webmentions-dark.CCKpzs_9.webp\" data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/received-webmentions-light.B3qD9kGF.webp\" alt=\"Admin page showing received webmentions for one post with delete buttons.\" loading=\"lazy\" decoding=\"async\" width=\"1248\" height=\"2301\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a></figure>\n<p>I didn’t want to figure out passwords and bearer auth, so the server does not attempt to do any authentication and just relies on Cloudflare Access to guard access to the UI and the endpoints this uses.</p>\n<p>Hopefully I never have to use it!</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/keeping-ai-crawlers-off-my-forgejo-server/",
      "url": "https://msfjarvis.dev/posts/keeping-ai-crawlers-off-my-forgejo-server/",
      "title": "Keeping AI crawlers off my Forgejo server",
      "date_published": "2026-06-29T04:53:18.854Z",
      "summary": "The short and bumbling journey to finally giving my tiny VPS some respite",
      "content_html": "<div class=\"markdown-alert markdown-alert-note\" dir=\"auto\">\n<p class=\"markdown-alert-title\" dir=\"auto\"><svg class=\"octicon\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>NOTE</p>\n<p>This post will mention Cloudflare. People have a lot of strong opinions about the company’s effective monopoly, myself included, but it was the fastest way for me to solve my immediate problem.\nI’m also employed at Cloudflare, and get a free Enterprise account from them, but I believe nothing I have set up here requires one. If someone tries this out and runs into any free-tier limitations, please let me know and I will make a correction.</p>\n</div>\n<p>Beginning 2026-06-18 I was starting to see a lot of slowness in my Forgejo instance, which I hoped would eventually subside, so I chose to just watch the load dashboards and wait.</p>\n<p>The crawlers obviously did not relent, so I started earnestly mitigating it on 2026-06-25 by enabling fail2ban which unfortunately did not do much to help the situation. I had to take a hammer to the problem and put in a Cloudflare <a href=\"https://developers.cloudflare.com/security/rules/\">security rule</a> banning the countries of China and Vietnam where most of the requests were originating. The rule also carried a specific ban for accesses to the <code>oneplus3</code> repository because it was surprisingly a “fan” favorite. The Cloudflare block immediately got to work and within 16 hours mitigated over 130K hits.</p>\n<p>However, the site’s availability started dropping over the next few hours and I started seeing a near constant stream of <code>HTTP 521</code> responses which indicates Cloudflare couldn’t reach my server. Turns out the log transformer I enabled for fail2ban to work wasn’t logging the real client IPs, it was logging Cloudflare IPs, so fail2ban had just been banning Cloudflare 😬</p>\n<p>Solving that wasn’t too difficult, Caddy has the necessary config knobs to mark <a href=\"https://caddyserver.com/docs/caddyfile/options#trusted-proxies\">reverse proxies as trusted</a> and <a href=\"https://caddyserver.com/docs/caddyfile/options#client-ip-headers\">forward the client IP headers</a> correctly. I unbanned all the IPs in fail2ban, and after that I followed <a href=\"https://github.com/fail2ban/fail2ban/blob/2c1eb69381796c49e8866bb26fdc057bca159bfd/config/action.d/cloudflare.conf\">this example</a> to make fail2ban directly block offenders via Cloudflare’s IP Access Rules.</p>\n<p>Within 3 hours I was up to 2200 individual IPs banned, so I decided to do some analysis of banned IPs to figure out if there’s a broader block I can instate and prevent running into the 50,000 limit. Most of them were popular consumer providers like Comcast and Verizon, so I banned the ASNs south of them:</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plaintext\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">- AS4229 Zenlayer</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">- AS150436 Byteplus</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">- AS62240 Clouvider</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">- AS62651 Strong Technology</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">- AS32934 Facebook</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"- AS4229 Zenlayer- AS150436 Byteplus- AS62240 Clouvider- AS62651 Strong Technology- AS32934 Facebook\"><div></div></button></div></figure></div>\n<p>This helped to some degree but not <em>quite</em>, and I was still getting a sustained 50 to a 100 RPS which wasn’t impossible to serve, just very wasteful and causing other things to slow down on this rather weak VPS. After around 12 hours, I realized fail2ban had now banned 31K individual IPs which was coming dangerously close to the 50K IP Access Rules limit from Cloudflare, and was admittedly a rather naive way of addressing the problem.</p>\n<p>I turned off fail2ban, and started poring through my Forgejo logs to find patterns I could let Cloudflare handle via a <a href=\"https://developers.cloudflare.com/ruleset-engine/custom-rulesets/\">custom ruleset</a>. This took a bit to figure out, but ultimately I landed on these, in order:</p>\n<ol>\n<li>Block China and Vietnam. These still have the worst of the residential proxy botnet problem.</li>\n<li>Block the known-bad ASNs from before.</li>\n<li>Block anyone accessing the <code>/oneplus3</code> and <code>/revanced-patches</code> repositories since they had become super attractive scraper honeypots for some reason, I don’t really need either to be available so this was fine.</li>\n<li>Block fetches of <code>.bundle</code> archives, since I don’t use them and they’re expensive to generate.</li>\n<li>Block (downgraded to Managed Challenge later) paginated history crawling:\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plaintext\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">http.request.method eq \"GET\" and</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">http.request.uri.path strict wildcard \"/*/*/commits/commit/*\" and</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">http.request.uri.query contains \"page=\"</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"http.request.method eq &quot;GET&quot; andhttp.request.uri.path strict wildcard &quot;/*/*/commits/commit/*&quot; andhttp.request.uri.query contains &quot;page=&quot;\"><div></div></button></div></figure></div>\n</li>\n<li>Managed Challenge for all other anonymous history crawling\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plaintext\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">http.request.method eq \"GET\" and</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">not cf.client.bot and</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">(</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">http.request.uri.path strict wildcard \"/*/*/blame/commit/*\" or</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">http.request.uri.path strict wildcard \"/*/*/commits/commit/*\" or</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">http.request.uri.path strict wildcard \"/*/*/src/commit/*\" or</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">http.request.uri.path strict wildcard \"/*/*/blame/tag/*\" or</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">http.request.uri.path strict wildcard \"/*/*/commits/tag/*\" or</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">http.request.uri.path strict wildcard \"/*/*/src/tag/*\" or</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">http.request.uri.path strict wildcard \"/*/*/find/tag/*\" or</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">http.request.uri.path strict wildcard \"/*/*/commit/*.diff\" or</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">http.request.uri.path strict wildcard \"/*/*/commit/*.patch\" or</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">(</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">http.request.uri.path strict wildcard \"/*/*/commit/*\" and</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">(</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">      </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">http.request.uri.query contains \"files=\" or</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">      </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">http.request.uri.query contains \"show-outdated=\" or</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">      </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">http.request.uri.query contains \"whitespace=\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">)</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"http.request.method eq &quot;GET&quot; andnot cf.client.bot and(  http.request.uri.path strict wildcard &quot;/*/*/blame/commit/*&quot; or  http.request.uri.path strict wildcard &quot;/*/*/commits/commit/*&quot; or  http.request.uri.path strict wildcard &quot;/*/*/src/commit/*&quot; or  http.request.uri.path strict wildcard &quot;/*/*/blame/tag/*&quot; or  http.request.uri.path strict wildcard &quot;/*/*/commits/tag/*&quot; or  http.request.uri.path strict wildcard &quot;/*/*/src/tag/*&quot; or  http.request.uri.path strict wildcard &quot;/*/*/find/tag/*&quot; or  http.request.uri.path strict wildcard &quot;/*/*/commit/*.diff&quot; or  http.request.uri.path strict wildcard &quot;/*/*/commit/*.patch&quot; or  (    http.request.uri.path strict wildcard &quot;/*/*/commit/*&quot; and    (      http.request.uri.query contains &quot;files=&quot; or      http.request.uri.query contains &quot;show-outdated=&quot; or      http.request.uri.query contains &quot;whitespace=&quot;    )  ))\"><div></div></button></div></figure></div>\n</li>\n<li>Managed Challenge for reading historical copies of files\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plaintext\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">http.request.method eq \"GET\" and</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">not cf.client.bot and</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">(</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">http.request.uri.path strict wildcard \"/*/*/raw/commit/*\" or</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">http.request.uri.path strict wildcard \"/*/*/raw/tag/*\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">)</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"http.request.method eq &quot;GET&quot; andnot cf.client.bot and(  http.request.uri.path strict wildcard &quot;/*/*/raw/commit/*&quot; or  http.request.uri.path strict wildcard &quot;/*/*/raw/tag/*&quot;)\"><div></div></button></div></figure></div>\n</li>\n</ol>\n<p>With all these in place the Git server is now <em>chilling</em>. I can’t remember the last time it was serving so few spam hits that I could see the Prometheus <code>/metrics</code> query in the journal logs.</p>\n<p>According to Cloudflare’s logs it has mitigated 489.65K requests for me since I deployed this rule set, and of the 100K Managed Challenge that have been issued, only 3 have been solved. Go BOTS team!</p>\n<p>Of course, I still had the problem of the now-useless IP Access Rules that fail2ban had deployed, which needed to be removed. Here I have some criticism of Cloudflare and the way the whole account/zone authentication situation works, because this took me over an hour to figure out and none of it was documented.</p>\n<p>I tried using an account API token to fetch IP Access Rules, and consistently got 0 results from the API. I tried a user API token and got the same result. I switched from the <code>/account</code> API shape to the <code>/zone</code> one, and started getting the completely useless <code>code: 10000 message: Authorization error</code> error. No matter how I tried to call the API, I would either get 0 or an error, while I was staring at 31K of them in the dashboard. Finally, I decided to try the Global API key whose docs has a big <a href=\"https://developers.cloudflare.com/fundamentals/api/get-started/keys/#limitations\">limitations section</a> but no mention that it’s the only way to access the supposed legacy IP Access Rules.</p>\n<p>So now that I had wasted an hour and could <em>finally</em> get back these 31K rules, I do a cursory analysis on them with MaxMind and obviously find no country or ASN I can do a simple ban on. To delete all these IP Access Rules I use the Cloudflare Python SDK to write a small script that would remove these serially since I didn’t wanna go for max speed and then have to handle being rate limited. This took a whole 8 hours and 49 minutes to chew through all those IP Access Rules, but given I was effectively doing 1 RPS off my residential Internet connection I’m not mad about it.</p>\n<p>Here’s some graphs to spruce up this word salad of a post.</p>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/cloudflare-security-rules.trlGj0cH.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/cloudflare-security-rules.trlGj0cH.webp\" data-pswp-width=\"1349\" data-pswp-height=\"465\" data-pswp-caption=\"The 0% there means nobody hitting the endpoint is solving the challenge, a.k.a that's all bots\" aria-label=\"Cloudflare security rules list for a Forgejo server, showing several active block and challenge rules for crawler traffic.\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/cloudflare-security-rules.trlGj0cH.webp\" alt=\"Cloudflare security rules list for a Forgejo server, showing several active block and challenge rules for crawler traffic.\" loading=\"lazy\" decoding=\"async\" width=\"1349\" height=\"465\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">The 0% there means nobody hitting the endpoint is solving the challenge, a.k.a that's all bots</figcaption></figure>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/grafana-cpu-usage.Dx4AyjCa.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/grafana-cpu-usage.Dx4AyjCa.webp\" data-pswp-width=\"1041\" data-pswp-height=\"448\" data-pswp-caption=\"No points for guessing when the ruleset was deployed\" aria-label=\"Grafana CPU usage chart with heavy load over several days, followed by a sharp drop to near idle levels.\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/grafana-cpu-usage.Dx4AyjCa.webp\" alt=\"Grafana CPU usage chart with heavy load over several days, followed by a sharp drop to near idle levels.\" loading=\"lazy\" decoding=\"async\" width=\"1041\" height=\"448\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">No points for guessing when the ruleset was deployed</figcaption></figure>"
    },
    {
      "id": "https://msfjarvis.dev/posts/taming-my-intimidating-rss-reader-queue/",
      "url": "https://msfjarvis.dev/posts/taming-my-intimidating-rss-reader-queue/",
      "title": "Taming my intimidating RSS reader queue",
      "date_published": "2026-06-18T07:22:00.000Z",
      "summary": "How I regained control of my reading habits",
      "content_html": "<p>I talked in <a href=\"https://msfjarvis.dev/weeknotes/week-24-2026/#:~:text=I%20checked%20my%20Miniflux%20unread%20count%20at%20the%20start%20of%20this%20week\">last week’s weeknote</a> about how I was struggling with getting my Miniflux reader situation under control, and what I was attempting to identify the problem. Today, I came across <a href=\"https://michal.sapka.pl/2026/no-more-rss/\">this blog post</a> from Michał Sapka (via <a href=\"https://mastodon.neilzone.co.uk/@neil/116769624379243503\">this Fediverse post</a>) talking about the exact feeling, and choosing to entirely drop RSS and rely on manually checking up on bookmarks instead.</p>\n<p>I think that was quite saddening to read, that the thing they had switched to as a way to get out of mindless algorithmic scrolling had ended up feeling like that itself. I’m personally not quite at the giving up stage, and over the weekend ended up making a bunch of changes within <a href=\"https://miniflux.app\">Miniflux</a> that have significantly improved my experience already.</p>\n<p>I’m going to preempt the weeknote and say this: the whole data driven analysis approach I was trying out last week was a complete waste of time and mental faculties. I had misdiagnosed my problem of not reading things as being tied to individual feeds which wasn’t <em>quite</em> accurate. “Bad” feeds do contribute to and exacerbate the problem, but ultimately the thing I found to be the real issue was <em>perception</em>.</p>\n<hr>\n<p>The feeds I subscribe to can broadly be categorized under the following labels</p>\n<ol>\n<li>News outlets: high volume, mix of intriguing and uninteresting content, leaning towards the latter on most days</li>\n<li>Prolific individuals posting multiple times a week, sometimes multiple times a day. <a href=\"https://unsung.aresluna.org/\">Unsung</a> is a perfect fit here.</li>\n<li>Slower paced and longer length posters.</li>\n<li><a href=\"https://www.wheresyoured.at/\">Ed Zitron</a>. He writes long insightful newsletters at frightening pace, and I am scared to put a label on this madman.</li>\n</ol>\n<p>The overwhelming feeling for me comes from the first category. It crowds the sorted-by-new unread queue and makes it look like the whole pile is just uninteresting things so you either close the tab/app or mark all as read and move on. The second problem is that cheeky <em>unread</em> count at the top of the screen which keeps reminding you that you’re a failure for not keeping up with this free knowledge you asked for.</p>\n<p>At least on Miniflux, these are both solvable problems, and it has helped me immensely in just the 3-4 days since I made these changes.</p>\n<p>For the first: Exclude the high volume/low signal feeds from the global unread list. Head over to Feeds &gt; Click ‘Edit’ on the one you want &gt; Check ‘Hide entries in global unread list’</p>\n<figure data-image-modal-root=\"\" class=\"half\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/hide-entries-in-global-queue.2bRLQ3Aq.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/hide-entries-in-global-queue.2bRLQ3Aq.webp\" data-pswp-width=\"398\" data-pswp-height=\"713\" data-pswp-caption=\"Hiding high volume feeds does wonders for your unread page\" aria-label=\"Feed settings page with the option to hide entries in the global unread list checked.\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/hide-entries-in-global-queue.2bRLQ3Aq.webp\" alt=\"Feed settings page with the option to hide entries in the global unread list checked.\" loading=\"lazy\" decoding=\"async\" width=\"398\" height=\"713\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Hiding high volume feeds does wonders for your unread page</figcaption></figure>\n<p>You can also hide entire categories if your feeds are already categorized</p>\n<figure data-image-modal-root=\"\" class=\"half\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/hide-category-in-global-queue.DZXgWlo6.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/hide-category-in-global-queue.DZXgWlo6.webp\" data-pswp-width=\"738\" data-pswp-height=\"267\" data-pswp-caption=\"Gone, just like that\" aria-label=\"Category settings page with the option to hide entries in the global unread list checked.\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/hide-category-in-global-queue.DZXgWlo6.webp\" alt=\"Category settings page with the option to hide entries in the global unread list checked.\" loading=\"lazy\" decoding=\"async\" width=\"738\" height=\"267\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Gone, just like that</figcaption></figure>\n<p>This removes the feed/category’s entries from the homepage while still letting me browse the posts individually in the feeds/category page when I am ready for my daily dose of Biz &amp; IT.</p>\n<p>Now the second problem: the ever looming unread counter haunting your dreams can simply be hidden with a bit of CSS. Click over to the ‘Settings’ option at the top and then paste snippet in the ‘Custom CSS’ section on that page, then click the ‘Update’ button.</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"css\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">.</span><span style=\"--0:#865511;--1:#B8BB26\">unread-counter-wrapper</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">#</span><span style=\"--0:#865511;--1:#FABD2F\">page-header-title</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">&gt;</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--1:#8EC07C\">span</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#116B70;--1:#FABD2F\">nth-child</span><span style=\"--0:#5D5F6E;--1:#B4A694\">(</span><span style=\"--0:#A54107;--1:#D995A8\">1</span><span style=\"--0:#5D5F6E;--1:#B4A694\">)</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FE8929\">display</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#D995A8\">none</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\".unread-counter-wrapper,#page-header-title > span:nth-child(1) {  display: none;}\"><div></div></button></div></figure></div>\n<p>With these changes you should find your default view of Miniflux a much more approachable space</p>\n<div class=\"gallery\" data-pswp-gallery=\"\" style=\"--gallery-min-width: 250px; --gallery-gap: 1.5rem\" data-astro-cid-rj65ajc3=\"\"><figure data-image-modal-root=\"\" class=\"half\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/unread-before-hiding.DP9Y6Ie_.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/unread-before-hiding.DP9Y6Ie_.webp\" data-pswp-width=\"913\" data-pswp-height=\"1340\" data-pswp-caption=\"The unread feed before changes, a big looming number over your head and a lot of relatively uninteresting things taking up most of your screen\" aria-label=\"Unread list in Miniflux before hiding one feed, showing more articles and a higher unread count.\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/unread-before-hiding.DP9Y6Ie_.webp\" alt=\"Unread list in Miniflux before hiding one feed, showing more articles and a higher unread count.\" loading=\"lazy\" decoding=\"async\" width=\"913\" height=\"1340\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">The unread feed before changes, a big looming number over your head and a lot of relatively uninteresting things taking up most of your screen</figcaption></figure><figure data-image-modal-root=\"\" class=\"half\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/unread-sans-counts.CChK6Ro3.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/unread-sans-counts.CChK6Ro3.webp\" data-pswp-width=\"913\" data-pswp-height=\"1340\" data-pswp-caption=\"A much cleaner experience with more focus on writing by individuals, and more attuned to what you'd like to naturally discover in a casual browsing\" aria-label=\"Unread list in Miniflux with article titles shown and unread count badges removed from the top bar.\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/unread-sans-counts.CChK6Ro3.webp\" alt=\"Unread list in Miniflux with article titles shown and unread count badges removed from the top bar.\" loading=\"lazy\" decoding=\"async\" width=\"913\" height=\"1340\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">A much cleaner experience with more focus on writing by individuals, and more attuned to what you'd like to naturally discover in a casual browsing</figcaption></figure></div>\n<p>No unread counter screaming at you, and your default view now has a significantly better signal-to-noise ratio. Hope this helps someone out in creating a more approachable feed reader experience for themselves!</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/i-guess-i-cook-now/",
      "url": "https://msfjarvis.dev/posts/i-guess-i-cook-now/",
      "title": "I guess I cook now",
      "date_published": "2026-04-01T09:26:00.000Z",
      "summary": "Learning to cook for myself went from necessity to hobby almost overnight",
      "content_html": "<blockquote>\n<p>This is a post for <a href=\"https://www.aprilcools.club/\">April Cools</a>, encouraging people to break out of their standard fare and write something new.</p>\n</blockquote>\n<p>For the longest time I’ve not had a particularly great inclination for cooking. I had lived at or very close to my home for practically all my life, I was pampered (spoiled) and was never really encouraged to pick it up. That had to change when I moved to Bengaluru as part of being acqui-hired by <a href=\"https://cloudflare.com\">Cloudflare</a> in early 2025.</p>\n<p>I moved into a 2 bedroom apartment with old friend and current colleague <a href=\"https://yashgarg.dev/\">Yash</a>, and we decided to get a home cook. Having been in a hotel for a month or so while house-hunting we had landed pretty squarely on not wanting to be dependent on food delivery. Through some neighbourly drama that is not worth unpacking, we had to fire our home cook pretty quickly and we found ourselves in the kitchen with a strong desire to not starve. We started with the basics: simple curries with chapattis. Chickpeas, black beans, paneer (cottage cheese), and potatoes were pretty common.</p>\n<p>The progress was honestly pretty slow. We started with using precooked chapattis sold in the supermarket for a few weeks before shame really set in. We bought a food processor (neither of us had the temperament for kneading dough by hand), and started making our own chapattis at home. We made a very early experimental foray into a <a href=\"https://en.wikipedia.org/wiki/Taro\">Taro root</a> (called “Arbi” in Hindi) based curry, a favorite of Yash’, which was an absolute nightmare to prepare and really killed the vibe on gastronomical experimentation for a while. Eventually we started doing pastas, fried rice, <a href=\"https://en.wikipedia.org/wiki/Pilaf\">pulao</a>, even a chicken curry where we learned the hard way why you’re supposed to thaw chicken 6 hours in advance (it was edible).</p>\n<p>My mother, who had kept me out of the kitchen all this time, found herself quite concerned for the well being of her pampered buffoon. For maybe 6 months I had to send a picture of my dinner every night so she could rest assured that I had a home cooked meal and wasn’t pounding McChickens three times a day. My photo gallery in the past year is probably 40% dinner plates by count. I’m not gonna pretend I took all of those to satisfy her and not to gloat to myself, but she definitely is the reason it started :)</p>\n<p>I was already part of the air fryer cult back at home, so when my friend <a href=\"https://subhrajyoti.me/\">Subhrajyoti</a> was moving out to the Netherlands I happily snapped up his air fryer. This unlocked a whole new dimension in the kitchen, and since then we haven’t gone a week without some type of frozen food in our freezer. The convenience is simply unbeatable, though Yash has not been a very big user of it so far. Some day he’ll see the light.</p>\n<p>Yash and I had been big fans of a hummus bowl sold by a food delivery service that was our staple breakfast at office for a while, and when they discontinued it I decided to finally look into how much effort it was to just make hummus myself. Turns out, not much! Hummus is just cooked chickpeas blended into a paste with maybe 5 ingredients added to it. I used the Christmas &lt;&gt; New Year’s week to try it out and ended up with a genuinely delicious batch.</p>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/day-1-hummus.D1OZBhss.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/day-1-hummus.D1OZBhss.webp\" data-pswp-width=\"3072\" data-pswp-height=\"4080\" data-pswp-caption=\"This was a bit of an arm workout not gonna lie\" aria-label=\"A food processor with blended chickpeas inside it, and a spatula sitting a little to the left that has clearly been inside the food processor. You can still see a bunch of solid chickpeas that are yet to be grounded down, floating in a slightly off white paste of ground chickpeas.\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/day-1-hummus.D1OZBhss.webp\" alt=\"A food processor with blended chickpeas inside it, and a spatula sitting a little to the left that has clearly been inside the food processor. You can still see a bunch of solid chickpeas that are yet to be grounded down, floating in a slightly off white paste of ground chickpeas.\" loading=\"lazy\" decoding=\"async\" width=\"3072\" height=\"4080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">This was a bit of an arm workout not gonna lie</figcaption></figure>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/day-1-hummus-result.BspyosDK.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/day-1-hummus-result.BspyosDK.webp\" data-pswp-width=\"3072\" data-pswp-height=\"4080\" data-pswp-caption=\"4/10 plating but 8/10 flavour, I'll take it.\" aria-label=\"A small metal plate with some hummus that I've done a poor job of garnishing with some olive oil, red chilli powder and cumin powder.\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/day-1-hummus-result.BspyosDK.webp\" alt=\"A small metal plate with some hummus that I've done a poor job of garnishing with some olive oil, red chilli powder and cumin powder.\" loading=\"lazy\" decoding=\"async\" width=\"3072\" height=\"4080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">4/10 plating but 8/10 flavour, I'll take it.</figcaption></figure>\n<p>The success of this experiment made me try this 3 more times to increasing levels of success, and also make my own falafels a couple of times. Still haven’t nailed that one, but got very close the last time!</p>\n<p>Yash also got us into making burritos ourselves, a repeat of the story with hummus but more driven by California Burrito being expensive rather than it disappearing altogether. It makes for something we can prep easily, lasts a couple of days, is quite healthy and very filling. Some highlights from the first burrito misadventures, before I resigned to just doing burrito bowls and skipping the embarrassment of not being able to wrap a tortilla very well.</p>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/the-birth-of-a-burrito.D3510n2H.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/the-birth-of-a-burrito.D3510n2H.webp\" data-pswp-width=\"4080\" data-pswp-height=\"3072\" data-pswp-caption=\"The internet told me this is how you make a burrito with two wraps.\" aria-label=\"Two tortillas on a metal plate, overlapping around 30% with a big pile of fillings on top. The internet told me this is the right way to make a burrito with two wraps.\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/the-birth-of-a-burrito.D3510n2H.webp\" alt=\"Two tortillas on a metal plate, overlapping around 30% with a big pile of fillings on top. The internet told me this is the right way to make a burrito with two wraps.\" loading=\"lazy\" decoding=\"async\" width=\"4080\" height=\"3072\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">The internet told me this is how you make a burrito with two wraps.</figcaption></figure>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/burrito-pan.Ck-DSyDc.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/burrito-pan.Ck-DSyDc.webp\" data-pswp-width=\"3072\" data-pswp-height=\"4080\" data-pswp-caption=\"One of the rare few burritos that stayed in one piece when we grilled them.\" aria-label=\"A burrito being lightly grilled on a pan. Shocking it has not split open its guts all over the place.\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/burrito-pan.Ck-DSyDc.webp\" alt=\"A burrito being lightly grilled on a pan. Shocking it has not split open its guts all over the place.\" loading=\"lazy\" decoding=\"async\" width=\"3072\" height=\"4080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">One of the rare few burritos that stayed in one piece when we grilled them.</figcaption></figure>\n<p>I bought a veggie chopper that lets me just make big piles of nutritious salads, topped with some air fried chicken sausages or whatever and just have an exceptionally filling dinner in under 20 minutes. Completely game changer for the days when I’m feeling lazy, has helped immensely with my struggle to lose weight.</p>\n<hr>\n<p>When I started going to the <a href=\"https://blr.indiewebclub.org/\">IndieWebClub</a> events late last year, I picked up the habit of publishing <a href=\"https://msfjarvis.dev/categories/weeknotes/\">weeknotes</a>. These have gradually started featuring food more prominently as I’ve gotten comfortable with simply trying things in the kitchen. Being able to navigate the kitchen with confidence has given me so much freedom that I previously thought to be out of my reach. Excited to see what other cuisines I’ll pick up this year!</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/setting-up-forward-auth-with-caddy-and-pocket-id/",
      "url": "https://msfjarvis.dev/posts/setting-up-forward-auth-with-caddy-and-pocket-id/",
      "title": "Setting up forward auth with Caddy and Pocket ID",
      "date_published": "2026-03-21T10:03:00.000Z",
      "summary": "Using Pocket ID to secure services being proxied by Caddy",
      "content_html": "<p>As I mentioned in my <a href=\"https://msfjarvis.dev/posts/weeknotes-week-11-2026/\">last weeknote</a>, I set up <a href=\"https://github.com/janeczku/calibre-web\">Calibre-Web</a> last week which necessitated the use of a forward authentication setup to work with my existing SSO provider. It was rather non-trivial to get it all to work, so I’m documenting it here in hopes of helping others.</p>\n<div class=\"markdown-alert markdown-alert-warning\" dir=\"auto\">\n<p class=\"markdown-alert-title\" dir=\"auto\"><svg class=\"octicon\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path></svg>WARNING</p>\n<p>05/05/2026: Since writing this post I’ve come to realize that caddy-security very frequently makes subtle breaking changes, and you only figure out the migration by creating an issue yourself or reading the GitHub issue someone else created. That’s far too much churn than I wish to have in security-sensitive software. I will personally be moving to Authelia and LLDAP in the near future to leverage its first party support for forward and reverse auth, and a more stable release cycle.</p>\n</div>\n<h2 id=\"requirements\">Requirements</h2>\n<ul>\n<li><a href=\"https://caddyserver.com/\">Caddy</a> with the <a href=\"https://github.com/greenpau/caddy-security/\">caddy-security</a> plugin</li>\n<li><a href=\"https://pocket-id.org/\">Pocket ID</a></li>\n<li>Patience.</li>\n</ul>\n<h2 id=\"pocket-id-and-caddy-security-setup\">Pocket ID and caddy-security setup</h2>\n<p>Follow the Caddy guide <a href=\"https://pocket-id.org/docs/guides/proxy-services\">here</a> to set up an OIDC client and the caddy-security configuration in your Caddyfile. This gets you 90% of the way, but due to recent regressions in caddy-security you’ll need to make some tweaks.</p>\n<p>First of all, in the <code>oauth identity provider</code> block, add this line:</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plain\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">trust login redirect uri domain exact ${app.domain} path prefix /</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"trust login redirect uri domain exact ${app.domain} path prefix /\"><div></div></button></div></figure></div>\n<p>Replace <code>${app.domain}</code> with the domain to the service you are securing.</p>\n<p>The guide also assumes you will re-use the same caddy-security authentication portal for all your services which is <em>fine</em>, but I prefer to have each OIDC client be isolated on a service level instead of just having a generic caddy-security one. I’ll explain the basic changes first then dive into the NixOS-specific stuff I did for my own deployment.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plain\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">security {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\"># Rename the provider from generic to the service name</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">oauth identity provider calibreweb {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">delay_start 3</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\"># Give this its own realm</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">realm calibreweb</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\"># This is the OIDC provider type, make sure this stays `generic`</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">driver generic</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\"># Service-scoped secrets from the Pocket ID OIDC client setup</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">client_id {$CALIBRE_WEB_POCKET_ID_CLIENT_ID}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">client_secret {$CALIBRE_WEB_POCKET_ID_CLIENT_SECRET}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">scopes openid email profile</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">base_auth_url https://auth.msfjarvis.dev</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">metadata_url https://auth.msfjarvis.dev/.well-known/openid-configuration</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">   </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\"># Rename the authentication portal as well</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">authentication portal calibreweb_portal {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">crypto default token lifetime 3600</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\"># Use the new name for the identity provider defined above</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">enable identity provider calibreweb</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">trust login redirect uri domain exact books.msfjarvis.dev path prefix /</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">cookie insecure off</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">cookie domain books.msfjarvis.dev</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">transform user {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">      </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\"># The new realm name</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">      </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">match realm calibreweb</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">      </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">action add role user</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\"># Per-service configuration</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">authorization policy calibreweb_policy {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">set auth url /caddy-security/oauth2/calibreweb</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">allow roles user</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">inject headers with claims</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"security {  # Rename the provider from generic to the service name  oauth identity provider calibreweb {    delay_start 3    # Give this its own realm    realm calibreweb    # This is the OIDC provider type, make sure this stays `generic`    driver generic    # Service-scoped secrets from the Pocket ID OIDC client setup    client_id {$CALIBRE_WEB_POCKET_ID_CLIENT_ID}    client_secret {$CALIBRE_WEB_POCKET_ID_CLIENT_SECRET}    scopes openid email profile    base_auth_url https://auth.msfjarvis.dev    metadata_url https://auth.msfjarvis.dev/.well-known/openid-configuration  }   # Rename the authentication portal as well  authentication portal calibreweb_portal {    crypto default token lifetime 3600    # Use the new name for the identity provider defined above    enable identity provider calibreweb    trust login redirect uri domain exact books.msfjarvis.dev path prefix /    cookie insecure off    cookie domain books.msfjarvis.dev    transform user {      # The new realm name      match realm calibreweb      action add role user    }  }  # Per-service configuration  authorization policy calibreweb_policy {    set auth url /caddy-security/oauth2/calibreweb    allow roles user    inject headers with claims  }}\"><div></div></button></div></figure></div>\n<p>With this setup you should be able to then copy-paste the code block above for a new service and just replace the name and URLs. Do note that the <code>security</code> block is globally unique, and only the contents inside it are meant to be duplicated.</p>\n<h3 id=\"sidebar-nixos-version\">Sidebar: NixOS version</h3>\n<blockquote>\n<p>People not using NixOS can ignore this section</p>\n</blockquote>\n<p>On NixOS I already have a <a href=\"https://github.com/msfjarvis/dotfiles/blob/d495c2afcc564b126a0ed1e89bbd8a2f2e3ff224/modules/nixos/caddy/default.nix\">Caddy module</a> that applies a set of defaults, so I retrofitted generating the security block into it. I added an option to let individual services configure the inputs via module options:</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"nix\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#91AFA3\">pocketIdApplications</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#E6928E\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">mkOption</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">type</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">types</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">attrsOf</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">(</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69\">    </span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">types</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">submodule</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--1:#FABD2F\">options</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--1:#FABD2F\">domain</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">mkOption</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">          </span><span style=\"--0:#1A57D1;--1:#FABD2F\">type</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">types</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">str</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">          </span><span style=\"--0:#1A57D1;--1:#FABD2F\">description</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Domain of the proxied service</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#5D5F6E;--1:#B4A694\">};</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--1:#FABD2F\">clientIdEnvVar</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">mkOption</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">          </span><span style=\"--0:#1A57D1;--1:#FABD2F\">type</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">types</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">str</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">          </span><span style=\"--0:#1A57D1;--1:#FABD2F\">description</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Environment variable name containing the client ID</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#5D5F6E;--1:#B4A694\">};</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--1:#FABD2F\">clientSecretEnvVar</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">mkOption</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">          </span><span style=\"--0:#1A57D1;--1:#FABD2F\">type</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">types</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">str</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">          </span><span style=\"--0:#1A57D1;--1:#FABD2F\">description</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Environment variable name containing the client secret</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#5D5F6E;--1:#B4A694\">};</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">};</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">);</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">default</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">};</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">description</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Applications to protect with Pocket ID OIDC via caddy-security</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#4C4F69;--1:#E6928E\">;</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"pocketIdApplications = mkOption {  type = types.attrsOf (    types.submodule {      options = {        domain = mkOption {          type = types.str;          description = &quot;Domain of the proxied service&quot;;        };        clientIdEnvVar = mkOption {          type = types.str;          description = &quot;Environment variable name containing the client ID&quot;;        };        clientSecretEnvVar = mkOption {          type = types.str;          description = &quot;Environment variable name containing the client secret&quot;;        };      };    }  );  default = { };  description = &quot;Applications to protect with Pocket ID OIDC via caddy-security&quot;;};\"><div></div></button></div></figure></div>\n<p>Then in the <code>services.caddy.globalConfig</code> option, you loop over the value of this to generate the config we wrote above.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"nix\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">lib</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">optionalString</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">(</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">config</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">services</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">caddy</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">pocketIdApplications</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">!=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">})</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">''</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">  </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">security {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">lib</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">concatStringsSep</span><span style=\"--0:#2C6E1E;--1:#B8BB26\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#8D477A;--1:#D995A8\">\\n</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">(</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#4C5AA8;--1:#91AFA3\">lib</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">mapAttrsToList</span><span style=\"--0:#2C6E1E;--1:#B8BB26\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">(</span><span style=\"--0:#AD343E;--0fs:italic;--1:#91AFA3\">name</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#2C6E1E;--1:#B8BB26\"> </span><span style=\"--0:#AD343E;--0fs:italic;--1:#91AFA3\">app</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#2C6E1E;--1:#B8BB26\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">''</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">        </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">oauth identity provider </span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">name</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#2C6E1E;--1:#B8BB26\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">delay_start 3</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">realm </span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">name</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">driver generic</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">client_id {</span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">app</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">clientIdEnvVar</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">client_secret {</span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">app</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">clientSecretEnvVar</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">scopes openid email profile</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">base_auth_url https://auth.msfjarvis.dev</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">metadata_url https://auth.msfjarvis.dev/.well-known/openid-configuration</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">        </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">}</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">        </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">authentication portal </span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">name</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">_portal {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">crypto default token lifetime 3600</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">enable identity provider </span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">name</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">trust login redirect uri domain exact </span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">app</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">domain</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#2C6E1E;--1:#B8BB26\"> path prefix /</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">cookie insecure off</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">cookie domain </span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">app</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">domain</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">transform user {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">            </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">match realm </span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">name</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">            </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">action add role user</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">        </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">}</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">        </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">authorization policy </span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">name</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">_policy {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">set auth url /caddy-security/oauth2/</span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">name</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">allow roles user</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">inject headers with claims</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">        </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E\">      </span></span><span style=\"--0:#2C6E1E;--1:#B4A694\">''</span><span style=\"--0:#5D5F6E;--1:#B4A694\">)</span><span style=\"--0:#2C6E1E;--1:#B8BB26\"> </span><span style=\"--0:#4C5AA8;--1:#91AFA3\">config</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">services</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">caddy</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">pocketIdApplications</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">)}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">  </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#2C6E1E;--1:#B4A694\">''</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"${lib.optionalString (config.services.caddy.pocketIdApplications != { }) ''  security {    ${lib.concatStringsSep &quot;\\n&quot; (      lib.mapAttrsToList (name: app: ''        oauth identity provider ${name} {          delay_start 3          realm ${name}          driver generic          client_id {${app.clientIdEnvVar}}          client_secret {${app.clientSecretEnvVar}}          scopes openid email profile          base_auth_url https://auth.msfjarvis.dev          metadata_url https://auth.msfjarvis.dev/.well-known/openid-configuration        }        authentication portal ${name}_portal {          crypto default token lifetime 3600          enable identity provider ${name}          trust login redirect uri domain exact ${app.domain} path prefix /          cookie insecure off          cookie domain ${app.domain}          transform user {            match realm ${name}            action add role user          }        }        authorization policy ${name}_policy {          set auth url /caddy-security/oauth2/${name}          allow roles user          inject headers with claims        }      '') config.services.caddy.pocketIdApplications    )}  }''}\"><div></div></button></div></figure></div>\n<p>With all this in place, you can then configure this alongside (in my case) Calibre-Web and have all the stuff above get generated without having to think too much about it:</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"nix\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#91AFA3\">sops</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">secrets</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">calibre-web-caddy-env</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#E6928E\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">sopsFile</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">lib</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">snowfall</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">fs</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">get-file</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">secrets/calibre-web.env</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">format</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">dotenv</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">owner</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">config</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">services</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">caddy</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">user</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">restartUnits</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">caddy.service</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">];</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#4C4F69;--1:#E6928E\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#91AFA3\">services</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">caddy</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">pocketIdApplications</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">calibreweb</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#E6928E\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">domain</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">cfg</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">domain</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">clientIdEnvVar</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">$CALIBRE_WEB_POCKET_ID_CLIENT_ID</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">clientSecretEnvVar</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">$CALIBRE_WEB_POCKET_ID_CLIENT_SECRET</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#4C4F69;--1:#E6928E\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#91AFA3\">systemd</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">services</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">caddy</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">serviceConfig</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">EnvironmentFile</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#E6928E\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69\">  </span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">config</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">sops</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">secrets</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">calibre-web-caddy-env</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">path</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">]</span><span style=\"--0:#4C4F69;--1:#E6928E\">;</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"sops.secrets.calibre-web-caddy-env = {  sopsFile = lib.snowfall.fs.get-file &quot;secrets/calibre-web.env&quot;;  format = &quot;dotenv&quot;;  owner = config.services.caddy.user;  restartUnits = [ &quot;caddy.service&quot; ];};services.caddy.pocketIdApplications.&quot;calibreweb&quot; = {  domain = cfg.domain;  clientIdEnvVar = &quot;$CALIBRE_WEB_POCKET_ID_CLIENT_ID&quot;;  clientSecretEnvVar = &quot;$CALIBRE_WEB_POCKET_ID_CLIENT_SECRET&quot;;};systemd.services.caddy.serviceConfig.EnvironmentFile = [  config.sops.secrets.calibre-web-caddy-env.path];\"><div></div></button></div></figure></div>\n<h2 id=\"caddy-routing-setup\">Caddy routing setup</h2>\n<p>Usually just throwing a <code>reverse_proxy localhost:&lt;port&gt;</code> gets you all the way with Caddy, but Calibre-Web specifically requires some extra configuration that serves for a nice demonstration. We need to treat different paths in different ways, so we’ll use the Caddy <a href=\"https://caddyserver.com/docs/caddyfile/directives/handle\"><code>handle</code> directive</a> to create mutually exclusive routing paths for them.</p>\n<p><code>caddy-security</code> adds a <code>/caddy-security</code> path in your sites that should always require authentication, so we simply route it to our portal without any changes.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plain\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">handle /caddy-security/* {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">route {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">authenticate with calibreweb_portal</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"handle /caddy-security/* {  route {    authenticate with calibreweb_portal  }}\"><div></div></button></div></figure></div>\n<p>Caddy lets you label a set of paths or with the <code>@name</code> syntax, this is just here for convenience. Calibre Web exposes OPDS and Kobo-specific endpoints for use with your devices that likely can’t do OIDC, so we allow those to bypass the authentication requirements. The transport buffer size was taken from <a href=\"https://github.com/janeczku/calibre-web/issues/1891\">https://github.com/janeczku/calibre-web/issues/1891</a> after I faced the same sync issues.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plain\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">@integrations {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">path /opds /opds/* /kobo /kobo/*</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">handle @integrations {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">reverse_proxy localhost:8080 {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">header_up X-Scheme https</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">transport http {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">      </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">read_buffer 1024k</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">      </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">write_buffer 1024k</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"@integrations {  path /opds /opds/* /kobo /kobo/*}handle @integrations {  reverse_proxy localhost:8080 {    header_up X-Scheme https    transport http {      read_buffer 1024k      write_buffer 1024k    }  }}\"><div></div></button></div></figure></div>\n<p>This is the catch-all route for everything else that doesn’t need special treatment. This only differs from the one above in requiring <em>authorization</em>.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plain\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">handle {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">route {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">authorize with calibreweb_policy</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">reverse_proxy localhost:8080 {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">      </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">header_up X-Scheme https</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">      </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">transport http {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">        </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">read_buffer 1024k</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">        </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">write_buffer 1024k</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">      </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"handle {  route {    authorize with calibreweb_policy    reverse_proxy localhost:8080 {      header_up X-Scheme https      transport http {        read_buffer 1024k        write_buffer 1024k      }    }  }}\"><div></div></button></div></figure></div>\n<p>The full configuration then becomes this:</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plain\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">handle /caddy-security/* {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">route {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">authenticate with calibreweb_portal</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">@integrations {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">path /opds /opds/* /kobo /kobo/*</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">handle @integrations {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">reverse_proxy localhost:8080 {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">header_up X-Scheme https</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">transport http {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">      </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">read_buffer 1024k</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">      </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">write_buffer 1024k</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">handle {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">route {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">authorize with calibreweb_policy</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">reverse_proxy localhost:8080 {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">      </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">header_up X-Scheme https</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">      </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">transport http {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">        </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">read_buffer 1024k</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">        </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">write_buffer 1024k</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">      </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">  </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"handle /caddy-security/* {  route {    authenticate with calibreweb_portal  }}@integrations {  path /opds /opds/* /kobo /kobo/*}handle @integrations {  reverse_proxy localhost:8080 {    header_up X-Scheme https    transport http {      read_buffer 1024k      write_buffer 1024k    }  }}handle {  route {    authorize with calibreweb_policy    reverse_proxy localhost:8080 {      header_up X-Scheme https      transport http {        read_buffer 1024k        write_buffer 1024k      }    }  }}\"><div></div></button></div></figure></div>\n<h3 id=\"sidebar-authentication-vs-authorization\">Sidebar: Authentication vs Authorization</h3>\n<p>This is something that took too long for me to hammer into my brain, so I’ll reproduce it here as well for others who may have had the same question but didn’t feel confident asking it.</p>\n<ul>\n<li>Authentication: Verifying your identity. This answers the question of who the user is.</li>\n<li>Authorization: Verifying your access. This answers the question of whether the previously identified user should be allowed through to a specific service.</li>\n</ul>\n<p>We require <em>authentication</em> for the <code>caddy-security</code> portal because you should be able to at least see that you have an account. The rest of the service requires <em>authorization</em> so only people who should be allowed access to the Calibre-Web instance are able to log in.</p>\n<hr>\n<p>And that’s the long and short of it! There was a lot of one-time effort in getting this working and I had to dig through a lot of GitHub issues to find bits and pieces that had to be tweaked, but I’m quite happy with how this all came together. I now have Calibre-Web syncing to my Kobo eReader, and no longer have to struggle with a split desktop library back at home and here in Bengaluru.</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/migrating-my-website-s-analytics-from-plausible-to-umami/",
      "url": "https://msfjarvis.dev/posts/migrating-my-website-s-analytics-from-plausible-to-umami/",
      "title": "Migrating my website's analytics from Plausible to Umami",
      "date_published": "2026-01-09T19:10:00.000Z",
      "summary": "Frustrated by ClickHouse's high disk usage, I ditched Plausible for Umami",
      "content_html": "<p>On January 5th I had another minor outage on one of my servers due to a full disk, caused in part by misconfigured Forgejo data dumps. In the process of fixing that I discovered that most of my server disk was actually being used by <a href=\"https://clickhouse.com\">ClickHouse</a>, which is holding data for my <a href=\"https://plausible.io\">Plausible</a> instance that records page views for this website.</p>\n<p>Why does it need 108 gigabytes to store 102K page views? I wish I could tell you. One of the developers of Plausible <a href=\"https://hachyderm.io/@ifthenelse/115843953290917862\">replied to my grumbling</a> explaining that it was likely ClickHouse collecting a ton of logs which was the real storage hog rather than the data itself. By that point I had already finished migrating off to <a href=\"https://umami.is/\">Umami</a> and deleted the offending ClickHouse data so I couldn’t verify the hypothesis.</p>\n<p>The migration itself was quite simple so I’ll reproduce the steps below for any interested people looking for migration steps.</p>\n<ul>\n<li>I <a href=\"https://git.msfjarvis.dev/msfjarvis/dotfiles/commit/323fccb9b16e805f665337284b8a4ae8313c0b6a\">set up the NixOS module for Umami</a>, and <a href=\"https://git.msfjarvis.dev/msfjarvis/dotfiles/commit/94419eb680752f573916b133d8ca6bd3162b5969\">enabled it</a> on a fresh subdomain to run it alongside Plausible during the migration.\n<ul>\n<li>Umami has this weird default of creating an admin account with the `admin:umami` credentials so make sure the first thing you do is change this password. I wish they would add OIDC support already so I could stop relying on this.</li>\n</ul>\n</li>\n<li>I logged into Plausible and following <a href=\"https://plausible.io/docs/export-stats#export-all-stats-to-date\">their docs</a>, obtained a ZIP file of all my data till date.</li>\n<li>Umami does not have any first-party support for importing data, so I had to use a <a href=\"https://github.com/JeongJuhyeon/plausible-to-umami\">third-party Python script</a> that converted my ZIP file of CSVs from Plausible into a 5 megabyte .sql file that I could import into the Umami database table. The conversion worked for me in the first try, which was quite the relief.</li>\n<li>With the SQL file in hand, I was able to easily import it into the running Umami instance by running <code>sudo -u umami psql -U umami -d umami &lt; plausible_migration.sql</code>. This worked flawlessly as well, and I was able to confirm on the Umami web interface that my data had been successfully imported.</li>\n<li>In my website, I <a href=\"https://git.msfjarvis.dev/msfjarvis/msfjarvis.dev/commit/b665eada40250af746d7f5212f6fba5d35086f3e\">switched the JS script</a> to the one I got from Umami, and updated the <a href=\"https://git.msfjarvis.dev/msfjarvis/msfjarvis.dev/commit/e5e4406b3863793eae4a8027b0ed64e92abc2018\">Content Security Policy headers</a> to match.</li>\n<li>Once this was deployed, I quickly clicked through the site and was able to see my session in Umami and confirm it was working.</li>\n</ul>\n<p>And that’s kind of it! After this I turned off Plausible and was able to delete the ClickHouse directory and reclaim the nearly half of my 256 GB disk it had been keeping hostage. Umami’s lack of OIDC and easy import options is definitely a sore point, but with how easy the whole thing ended up being for me I can live with it for the time being. Interestingly, <a href=\"https://msfjarvis.dev/posts/migrating-from-simple-analytics-to-self-hosted-plausible/\">Plausible only lasted 8 months</a> before I moved out of it. I should’ve been more wary of ClickHouse than I was of MySQL when I chose Plausible over Matomo :)</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/coming-around-on-the-utility-of-llms/",
      "url": "https://msfjarvis.dev/posts/coming-around-on-the-utility-of-llms/",
      "title": "Coming around on the utility of LLMs",
      "date_published": "2026-01-02T16:53:00.000Z",
      "summary": "Having unsuccessfully tried to use Windsurf and GitHub Copilot over the years, OpenCode finally made LLMs useful to me",
      "content_html": "<blockquote>\n<p>LLMs are an extremely divisive topic so I hesitated for a while before writing this post but reading <a href=\"https://www.joanwestenberg.com/the-case-for-blogging-in-the-ruins/\">this blog</a> from Joan Westenberg convinced me to not worry too much about what I put on my own blog.</p>\n</blockquote>\n<p>For the longest time I’ve found LLMs to be quite underwhelming. I tried all the new and fancy models on and off, first via <a href=\"https://github.com/copilot\">GitHub Copilot</a> which I get for free for being an active open source maintainer, and then with Windsurf at <a href=\"https://cloudflare.com\">work</a>. The sycophancy was annoying, the training data was always outdated, and the models were yet to learn how to accept they had no fucking idea about the thing I asked.</p>\n<p>That only came to change very recently, with <a href=\"https://github.com/sst/opencode\">OpenCode</a>. We have access to it at work via the <a href=\"https://www.cloudflare.com/developer-platform/products/ai-gateway/\">Cloudflare AI Gateway</a> and the latest Anthropic models.</p>\n<p>OpenCode is quite capable by default, with sensible defaults and a functional UI to go with it. What really unlocked its capabilities for me was <a href=\"https://github.com/code-yeongyu/oh-my-opencode\">oh-my-opencode</a>, a plugin that supercharges OpenCode by giving it the capabilities to launch background tasks via “agents” that enhance its capabilities across specific dimensions such as online research, UI design and system design. It adds integrations with the <a href=\"https://microsoft.github.io/language-server-protocol/\">Language Server Protocol</a> to ensure edits don’t introduce diagnostic errors. It includes a comment checker to prevent the typical annoyance of over-commenting code and giving it the distinct LLM smell. It bundles a set of curated <a href=\"https://modelcontextprotocol.io/docs/getting-started/intro\">MCP servers</a> to enhance its ability to do research: <a href=\"https://docs.exa.ai/reference/exa-mcp\">Exa</a> to do web searches, <a href=\"https://context7.com/\">Context7</a> to find up-to-date documentation, and <a href=\"https://grep.app\">Grep</a> to efficiently search through code on GitHub. All of these complicated bells and whistles come together to become a cohesive tool that can tackle both easy and difficult programming tasks with a very high degree of reliability.</p>\n<p>I’ve only started using OpenCode outside work in the past week or two, but the value add is undeniable. Here’s a non-exhaustive list of things I used OpenCode for in <a href=\"https://github.com/msfjarvis/compose-lobsters\">Claw</a>, with varying degrees of complexity and impact.</p>\n<ul>\n<li>It fixed <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/9c05e9b2e2364fd7a43ecc3d1dd896ae71ad09e0\">a crash</a> that has hit almost every user at some point in the past two years.</li>\n<li>It helped me add <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/7f5dbf79e370e586068c3d2a5031a8d199d398f2\">Sentry instrumentation for the app’s database</a> which was previously missing and I kept struggling to understand the right APIs for it.</li>\n<li>It helped write an <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/6d0953742b9036620d10b5fcc221050290d3efb0\">end-to-end UI testing</a> suite that ensured common flows didn’t break during my rowdy refactoring.</li>\n<li>It fixed <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/b3373473a79679fb1d260cbd1219fd0208f82bf3\">another crash</a> that hit users of the app’s home screen widget</li>\n<li>It fixed <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/7f0acae2da776c3c0c598cf282d5a74220f264a5\">yet another crash</a> caused by not handling process death correctly (I can already hear the groans of Android devs worldwide)</li>\n<li>It fixed <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/ab872b6eaa3c8aa8e5e751f15373f4f5ae1023a5\">my poorly written deep linking implementation</a></li>\n<li>It spent an hour taking apart the internals of the <a href=\"https://developer.android.com/topic/libraries/architecture/paging/v3-overview\">AndroidX Paging3</a> library to fix <a href=\"https://github.com/msfjarvis/compose-lobsters/commit/7f608fd536a6f58ab1cbf47f5bc634e09f713c04\">a performance regression</a> caused by the first fix.</li>\n</ul>\n<p>None of these are things that would be impossible for me to figure out, but they sure as hell wouldn’t have been done in the span of two weeks. I don’t really subscribe to the idea of fully phoning it in, the so called “vibecoding” aspect of using an LLM as a coding assistant. In every case where the LLM has solved a bug, I’ve learned something new from the fix and even went beyond in many cases because of the leg up I got by using the LLM.</p>\n<p>With the <a href=\"https://docs.sentry.io/product/sentry-mcp/\">Sentry MCP</a> server, I can spin up an OpenCode session, say “Investigate the root cause for Sentry issue &lt;id&gt; and include references in the summary” and go off to do <a href=\"https://bookwyrm.social/user/msfjarvis\">something else</a>. When I get back, I have a better understanding of the issue in question, I can use the references to do my own research and tell the LLM to implement one of potentially multiple ways to fix the bug. In many situations, writing the actual code is just not the more fun part of the problem.</p>\n<p>That said, I do want to clarify that I still find writing code to be enjoyable or fulfilling in most cases even with an LLM in the picture. <a href=\"https://github.com/msfjarvis/compose-lobsters/pull/1005\">This pull request</a> was mostly me doing a bulk find -&gt; replace across the codebase, something an LLM would excel at. However, I believe I am more capable of learning about <a href=\"https://github.com/ZacSweers/metro/\">Metro</a>’s capabilities ahead of time and spotting opportunities to replace existing constructs with Metro-native implementations, so I did it by hand and manually reviewed each file.</p>\n<p>Just a few months ago, I would not have believed that I could use LLMs to write any code I intended to actually maintain. Similar to Simon Willison’s <a href=\"https://tools.simonwillison.net/\">tools.simonwillison.net</a>, I had also set myself up with a <a href=\"https://git.msfjarvis.dev/msfjarvis/acceptable-vibes/\">small repo</a> and a <a href=\"https://vibes.msfjarvis.dev/\">public page</a> that hosted those tools. These things still have the default LLM sheen to them, and I gave up on even <a href=\"https://git.msfjarvis.dev/msfjarvis/acceptable-vibes/commit/ab038673622fea5e0a1ee0132caa9386735c796b\">having them follow a personalized code style</a>. For all definitions of the phrase, it is vibe coding. They solve simple problems for me, and will likely never break or need an update.</p>\n<p>Despite my changed opinions about LLMs as programming assistants, I am vehemently against the anthropomorphizing of LLM-based chatbots. They do very <a href=\"https://www.bbc.com/news/articles/cgerwp7rdlvo\">real harm</a>, and should be very aggressively regulated. I hold an even stronger belief that image and video generation models should not be available to the general public at all. The fact that humans can no longer trust even their eyes is deeply disturbing to me. It has been proven that these technologies will actively <a href=\"https://www.pcgamer.com/software/ai/apparently-the-most-popular-clip-on-openais-new-ai-video-app-sora-depicts-sam-altman-stealing-graphics-cards/\">be misused for deceit</a> and in the case of Elon Musk’s xAI, <a href=\"https://www.seangoedecke.com/grok-deepfakes/\">sexual harassment of minors</a>.</p>\n<p>This is close to devolving into rambling, so I’ll end it here. I don’t intend to form a crippling reliance on LLMs anytime soon, nor start paying for them. As long as GitHub is willing to continue wasting money on giving me access to LLMs for using their website, I’ll use them for my personal projects. When they stop, I’ll just go back to the good old elbow grease that has got me this far. If I can write Java over SSH into <a href=\"https://nano-editor.org/\">nano</a>, I think I can still use a search engine by hand.</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/my-experience-at-droidcon-india-2025/",
      "url": "https://msfjarvis.dev/posts/my-experience-at-droidcon-india-2025/",
      "title": "My experience at droidcon India 2025",
      "date_published": "2025-12-14T06:47:00.000Z",
      "summary": "I finally stepped out of my house! And met people! So cool!",
      "content_html": "<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/droidcon-2025-badge.CKOz7EVw.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/droidcon-2025-badge.CKOz7EVw.webp\" data-pswp-width=\"3072\" data-pswp-height=\"4080\" data-pswp-caption=\"My droidcon 2025 badge\" aria-label=\"My droidcon 2025 badge with my name, company and role.\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/droidcon-2025-badge.CKOz7EVw.webp\" alt=\"My droidcon 2025 badge with my name, company and role.\" loading=\"lazy\" decoding=\"async\" width=\"3072\" height=\"4080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">My droidcon 2025 badge</figcaption></figure>\n<p>I was at droidcon India <a href=\"https://india.droidcon.com/agenda\">this Saturday</a> and got the chance to meet a bunch of people I hadn’t seen in a while and attend some really great talks. It was a great experience and definitely motivated me to come out for meetups and events a bit more and be less of a loner :P</p>\n<p>Before I start walking through my day, I would like to extend an apology to my friends who accompanied me to the conference and the Android developer community at large. I might be the singular dumbass who would be at an event where both <a href=\"https://x.com/aditlal\">Adit Lal</a> and <a href=\"https://jitinsharma.com/\">Jitin Sharma</a> were speaking and plan an itinerary that included neither of their talks. I am extremely sorry and will never live this down.</p>\n<p>We arrived at the <a href=\"https://www.hilton.com/en/hotels/blrkrci-conrad-bengaluru/\">Conrad Bengaluru</a> a little before 9 AM to an already bustling space and settled into the Fireside Chat with Google’s DevRel Team. Lots of talk about AI as expected, but also the topic of passkeys and <a href=\"https://developer.android.com/identity/credential-manager\">Credential Manager</a> potentially doing away with the ridiculous OTP authentication flows used by banks here in India (🤞 one can hope).</p>\n<p>After a short break we attended our first talk, <a href=\"https://www.linkedin.com/in/suresh-maidaragi/\">Suresh Maidaragi</a> speaking about Building Mobile Apps at Scale with Kotlin Multiplatform. It was a case study on their experience at Physics Wallah of unifying a lagging iOS app with their higher development velocity Android app via KMP. I was interested in learning some new things here as my work has involved a <a href=\"https://realtime.cloudflare.com/\">Kotlin Multiplatform powered SDK</a> for ~2 years now, but was left a little disappointed as the talk was rather shallow on technical details. I was rather <del>surprised</del> horrified to hear that their app takes <em>2 hours</em> to build in their release CI with their current setup!</p>\n<p>Another short break and then one of my highlights for the day: <a href=\"https://www.linkedin.com/in/rahulrav/\">Rahul Ravikumar</a>’s “A Busy Android App Developer’s Guide to Perfetto”. Inspired by <a href=\"https://lalitm.com/\">Lalit Maganti</a>’s recent writing I had been teaching myself how to use Perfetto to debug some performance issues at work, so this was perfect for me and the colleagues I had dragged along to the conference (I joke, they came willingly. I think). This talk was very information-dense, I learned a lot about navigating Perfetto and making sense of its seemingly endless UI components. We got a sneak peek at the in-development <a href=\"https://github.com/androidx/androidx/tree/c347e7ecf3ea6dcdb302ee0f641b6409a44c4f33/tracing/tracing\">Tracing 2.0</a> library and all the problems it’s solving from Tracing 1. Especially exciting was <a href=\"https://github.com/androidx/androidx/tree/c347e7ecf3ea6dcdb302ee0f641b6409a44c4f33/tracing/tracing\">context propagation</a>, which can connect parent coroutines to their children in the Perfetto UI allowing you to effectively trace multistep async operations.</p>\n<p>The next talk I attended was <a href=\"https://www.linkedin.com/in/himanshoe/\">Himanshu Singh</a> speaking about “Building Android Open Source Libraries: Managing Public APIs with Intention”. Another talk I found to be rather underwhelming, as the synopsis in the agenda made me assume this would carry a little more technical content than it ended up with. Himanshu mostly spoke about the social aspects of starting and managing open source projects in general before mentions of <a href=\"https://github.com/kotlin/binary-compatibility-validator\">Kotlin BCV</a> and release automation towards the end.</p>\n<p>After the talk we had a lunch break, and I got to catch up with <a href=\"https://pratul.com/\">Pratul Kalia</a> who used to be my CTO at <a href=\"https://www.obvious.in/\">Obvious</a>. He’s now building <a href=\"https://tramline.app\">Tramline</a> to make mobile releases as streamlined as deploying to the web. I’m a happy solo user of the product, but am otherwise not paid to shill it here :P</p>\n<p>We skipped the 1:30 PM to 2:00 PM time slot since and just hung out since none of the talks particularly appealed to our small group of 4.</p>\n<p>At 2:00 PM we headed to <a href=\"https://www.linkedin.com/in/patilsid/\">Sid Patil</a> and <a href=\"https://www.linkedin.com/in/rutvikbhatt/\">Rutvik Bhatt</a>’s talk: “The Art of Re-architecture: Lessons from the Trenches at Foodpanda &amp; Delivery Hero”. As someone currently in the weeds of planning a re-architecture myself, their deep insights on how to evaluate the desired outcomes and engaging stakeholders of an engineering investment like this were very helpful. The mantra of “Refactor for today, Re-architect for tomorrow” was particularly inspiring as the resident refactorman of my team 👌</p>\n<p>Next up was <a href=\"https://linkedin.com/in/hadiyarajesh\">Rajesh Hadiya</a> with “Build Kotlin Compiler Plugins for Production level Android Apps”. Despite the extremely dense topic he did a great job at making it fun and approachable. I’ve tried to solve a logging-related problem earlier last year with a <a href=\"https://github.com/msfjarvis/tracelog\">Kotlin compiler plugin</a> so I was pretty familiar with the pain involved in dealing with the Kotlin IR and the various compiler backends. Rajesh was able to explain the compiler fundamentals that enable the various frontend/backend compiler plugins work the way they do in a succinct manner.</p>\n<p>We skipped the 3:45 PM to 04:05 PM slot as well due to nothing particularly appealing to the group.</p>\n<p><a href=\"https://www.linkedin.com/in/omkartenkale/\">Omkar Tenkale</a>’s talk titled “Building the Coroutine Framework from Scratch” fully delivered on the premise. We started by talking about the distinctions between <code>kotlinx.coroutines</code> and <code>kotlin.coroutines</code>, then fully throwing away the former and just relying on the standard library + compiler intrinsics to rebuild the primitives we’ve come to rely on from <code>kotlinx.coroutines</code> such as <code>runBlocking</code>, <code>launch</code>, <code>Dispatchers</code> , <code>GlobalScope</code> and such. The talk was packed to the gills with low level details yet was very approachable and I left the room a lot more knowledgeable and appreciative of <code>kotlinx.coroutines</code>! Omkar’s was easily my second favorite talk here after Rahul’s Perfetto deep dive.</p>\n<p>The last talk we attended was <a href=\"https://www.linkedin.com/in/nikheel-savant/\">Nikheel Vishwas Savant</a> from Meta talking about their experience building up a “Cross-Platform Bluetooth Architecture for Android, iOS, and Embedded Devices” for the Meta RayBan glasses. It was expected yet disappointing that we didn’t see any code in this talk, but there were a lot of great nuggets of information that I expect to take back to my work on RealtimeKit.</p>\n<p>And that was it for droidcon! By 5:30 PM we were all pretty exhausted so I bid my goodbyes to the people staying back and explored the nearby Church Street area with a couple of friends before heading home.</p>\n<p>This was my first proper con-style event in a <em>long</em> time (the last time I was at one was <a href=\"https://developers.google.com/events/gdd-india/schedule/day1\">Google Developer Days</a> back in 2017!) so I was somewhat nervous but it ended up being a great time. I missed a few people who were also there which was sad, but now that I’m in Bengaluru full time I hope to take more opportunities to show up at meetups and see people more than once every 3 years :)</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/creating-private-services-on-nixos-using-tailscale-and-caddy/",
      "url": "https://msfjarvis.dev/posts/creating-private-services-on-nixos-using-tailscale-and-caddy/",
      "title": "Creating private services on NixOS using Tailscale and Caddy",
      "date_published": "2025-09-13T15:48:00.000Z",
      "summary": "A simple guide to setting up private services on NixOS using Tailscale and Caddy with authentication.",
      "content_html": "<p><a href=\"https://tailscale.com\">Tailscale</a> is a mesh VPN that makes it dead simple to connect almost any device together in a private network. <a href=\"https://caddyserver.com\">Caddy</a> is a web server that focuses on ease of use and automatic HTTPS. I am a fan of both of these, and I was very excited to discover that Tailscale has an experimental <a href=\"https://github.com/tailscale/caddy-tailscale\">integration with Caddy</a> that leverages their <a href=\"https://tailscale.com/kb/1244/tsnet\"><code>tsnet</code></a> library to allow creating unique Tailscale addresses for individual virtual hosts in your Caddy configuration. Here’s a quick run down of how to set this up on NixOS.</p>\n<h2 id=\"installing-caddy-tailscale-for-nixos\">Installing caddy-tailscale for NixOS</h2>\n<p>As of the latest NixOS release, the Caddy package in nixpkgs supports including plugins directly which greatly simplifies this step. As an example, here’s how I include the caddy-tailscale plugin with the excellent <a href=\"https://github.com/JasonLovesDoggo/caddy-defender\">caddy-defender</a> that lets me drop traffic from AI scrapers more safely.</p>\n<blockquote>\n<p>You can also keep the package definition in a separate file <a href=\"https://github.com/msfjarvis/dotfiles/blob/94b443ce6748a1897b7b839e1564eca34bfcbe3e/packages/caddy-with-plugins/default.nix\">like this</a>, and use <a href=\"https://github.com/msfjarvis/dotfiles/blob/94b443ce6748a1897b7b839e1564eca34bfcbe3e/dev/caddy/update_caddy_plugins.py\">this script</a> to keep it up-to-date. DISCLAIMER: The script was vibe coded by Claude, all I’ve done is checked it for obviously silly shit.</p>\n</blockquote>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">services.caddy = {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">enable = true;</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">package =</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">pkgs.caddy.withPlugins {</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">      </span></span><span style=\"--1:#EBDBB2\">plugins = [</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">        </span></span><span style=\"--1:#EBDBB2\">\"github.com/jasonlovesdoggo/caddy-defender@v0.8.5\"</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">        </span></span><span style=\"--1:#EBDBB2\">\"github.com/tailscale/caddy-tailscale@v0.0.0-20250207163903-69a970c84556\"</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">      </span></span><span style=\"--1:#EBDBB2\">];</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">      </span></span><span style=\"--1:#EBDBB2\">hash = \"sha256-z+zj3rfXbyxldRjO1yoLD77ACRWEAofzMDiZe/bHAqw=\";</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">   </span></span><span style=\"--1:#EBDBB2\">}</span></span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">globalConfig = ''</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">servers {</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"services.caddy = {  enable = true;  package =    pkgs.caddy.withPlugins {      plugins = [        &quot;github.com/jasonlovesdoggo/caddy-defender@v0.8.5&quot;        &quot;github.com/tailscale/caddy-tailscale@v0.0.0-20250207163903-69a970c84556&quot;      ];      hash = &quot;sha256-z+zj3rfXbyxldRjO1yoLD77ACRWEAofzMDiZe/bHAqw=&quot;;   }  globalConfig = ''    servers {\"><div></div></button></div></figure></div>\n<p>This will swap out the <code>caddy</code> binary in your NixOS configuration with one that has the <code>caddy-tailscale</code> module patched in. You will also need to provide a Tailscale authkey to allow the <code>caddy-tailscale</code> module to interact with the Tailscale API and provision new nodes in your Tailnet. This can be done by setting the <code>TS_AUTHKEY</code> environment variable in your Caddy service configuration. Here’s an example of how to do so using <a href=\"https://github.com/Mic92/sops-nix\"><code>sops-nix</code></a>:</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"nix\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#91AFA3\">sops</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">secrets</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">services-tsauthkey-env</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#E6928E\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">sopsFile</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#B8BB26\">./secrets/tailscale.env</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">owner</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">config</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">services</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">caddy</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">user</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">format</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">dotenv</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#4C4F69;--1:#E6928E\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#91AFA3\">services</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">caddy</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">environmentFile</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#E6928E\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">config</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">sops</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">secrets</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">services-tsauthkey-env</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">path</span><span style=\"--0:#4C4F69;--1:#E6928E\">;</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"sops.secrets.services-tsauthkey-env = {  sopsFile = ./secrets/tailscale.env;  owner = config.services.caddy.user;  format = &quot;dotenv&quot;;};services.caddy.environmentFile = config.sops.secrets.services-tsauthkey-env.path;\"><div></div></button></div></figure></div>\n<p>Create the <code>secrets/tailscale.env</code> file with sops, the contents should look something like this:</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plaintext\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">TS_AUTHKEY=tskey-auth-something-something</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"TS_AUTHKEY=tskey-auth-something-something\"><div></div></button></div></figure></div>\n<h2 id=\"setting-up-your-first-private-service\">Setting up your first private service</h2>\n<p>Now we’re ready to rumble. To showcase the full capabilities of <code>caddy-tailscale</code>, we’re going to set up <a href=\"https://grafana.com\">Grafana</a> with <a href=\"https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-authentication/auth-proxy/\">proxy authentication</a> and let Tailscale <a href=\"https://github.com/tailscale/caddy-tailscale?tab=readme-ov-file#authentication-provider\">handle the authentication</a> for us. Here’s a simple example (N.B. <code>$TAILNET_NAME</code> should be replaced with your Tailscale network name.):</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"nix\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#91AFA3\">services</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">caddy</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">virtualHosts</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#E6928E\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">https://</span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">config</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">services</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">grafana</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">settings</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">server</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">domain</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#FABD2F\">extraConfig</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">''</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">      </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\"># This will create a new node at grafana.$TAILNET_NAME.ts.net</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">      </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">bind tailscale/grafana</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">      </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\"># Enables the Tailscale authentication provider</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">      </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">tailscale_auth</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">      </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\"># Forwards your Tailscale user ID to Grafana. Usually your email address.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">      </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">reverse_proxy </span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">config</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">services</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">grafana</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">settings</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">server</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">http_addr</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">:</span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--1:#B8BB26\"><span style=\"--0:#1A57D1;--0fs:italic\">toString</span><span style=\"--0:#2C6E1E\"> </span></span><span style=\"--0:#4C5AA8;--1:#91AFA3\">config</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">services</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">grafana</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">settings</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">server</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">http_port</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#2C6E1E;--1:#B8BB26\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">        </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">header_up X-Webauth-User {http.auth.user.tailscale_user}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">      </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E\">    </span></span><span style=\"--0:#2C6E1E;--1:#B4A694\">''</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">};</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#4C4F69;--1:#E6928E\">;</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#91AFA3\">services</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">grafana</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#E6928E\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">enable</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--1:#D995A8\">true</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">settings</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">auth.proxy</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Enable proxy-based authentication</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--1:#FABD2F\">enabled</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--1:#D995A8\">true</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Automatically create new accounts for people who access the service</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--1:#FABD2F\">auto_sign_up</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--1:#D995A8\">true</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Do not store login cookies, instead always relying on proxy authentication</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--1:#FABD2F\">enable_login_token</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--1:#D995A8\">false</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">};</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#FABD2F\">server</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--1:#FABD2F\">domain</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">grafana.$TAILNET_NAME.ts.net</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--1:#FABD2F\">http_addr</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">127.0.0.1</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--1:#FABD2F\">http_port</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">2342</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">};</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">};</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#4C4F69;--1:#E6928E\">;</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"services.caddy.virtualHosts = {  &quot;https://${config.services.grafana.settings.server.domain}&quot; = {    extraConfig = ''      # This will create a new node at grafana.$TAILNET_NAME.ts.net      bind tailscale/grafana      # Enables the Tailscale authentication provider      tailscale_auth      # Forwards your Tailscale user ID to Grafana. Usually your email address.      reverse_proxy ${config.services.grafana.settings.server.http_addr}:${toString config.services.grafana.settings.server.http_port} {        header_up X-Webauth-User {http.auth.user.tailscale_user}      }    '';  };};services.grafana = {  enable = true;  settings = {    &quot;auth.proxy&quot; = {      # Enable proxy-based authentication      enabled = true;      # Automatically create new accounts for people who access the service      auto_sign_up = true;      # Do not store login cookies, instead always relying on proxy authentication      enable_login_token = false;    };    server = {      domain = &quot;grafana.$TAILNET_NAME.ts.net&quot;;      http_addr = &quot;127.0.0.1&quot;;      http_port = 2342;    };  };};\"><div></div></button></div></figure></div>\n<p>This configuration will set up a new Grafana instance at <code>https://grafana.$TAILNET_NAME.ts.net</code> that is only accessible to members of your Tailscale network. When you visit the URL, your Tailscale identity will be used to log you into your existing Grafana account, or sign you up if this is your first time visiting the service.</p>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/grafana-profile.C-d3OEw7.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/grafana-profile.C-d3OEw7.webp\" data-pswp-width=\"1959\" data-pswp-height=\"780\" data-pswp-caption=\"Grafana profile showing that user identification is being done via the Tailscale proxy\" aria-label=\"The profile page on Grafana, showing the name, email and username fields with an additional 'synced via auth proxy' label next to them. The email and username have been blurred out to redact them for privacy reasons\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/grafana-profile.C-d3OEw7.webp\" alt=\"The profile page on Grafana, showing the name, email and username fields with an additional 'synced via auth proxy' label next to them. The email and username have been blurred out to redact them for privacy reasons\" loading=\"lazy\" decoding=\"async\" width=\"1959\" height=\"780\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Grafana profile showing that user identification is being done via the Tailscale proxy</figcaption></figure>\n<h2 id=\"caveats\">Caveats</h2>\n<p>While this approach works great for most needs, there are a few caveats to be aware of:</p>\n<ul>\n<li>Each service created this way registers as a new device in your Tailnet. Make sure to remain aware of the devices limit on your account.</li>\n<li>Since each “virtual” device being created via this is technically backed by your physical machine, the “Services” tab in your Tailscale dashboard will end up with duplicate entries of services that are running on the same machine. For example, I run 5 <code>caddy-tailscale</code> services on my server, which results in 6 entries of the SSH service being listed. This may be a deal breaker to you, depending on how you use the Tailscale dashboard.</li>\n</ul>\n<h2 id=\"conclusion\">Conclusion</h2>\n<p>Despite its experimental status, <code>caddy-tailscale</code> is pretty powerful and exposes enough functionality to cover most basic use cases. I’ve been pretty happy running it in my homelab for the past couple months and I’m excited to see how it evolves in the future. If you have any questions or corrections, please let me know either in the comments below or on <a href=\"https://fantastic.earth/@msfjarvis\">Mastodon</a>.</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/migrating-from-gitea-to-forgejo-the-long-way/",
      "url": "https://msfjarvis.dev/posts/migrating-from-gitea-to-forgejo-the-long-way/",
      "title": "Migrating from Gitea to Forgejo the long way",
      "date_published": "2025-09-01T14:02:00.000Z",
      "summary": "With the renewed interest in Forgejo I decided to finally pull the plug on moving out of Gitea, and this is how it went.",
      "content_html": "<p>I’ve had a Git server at <a href=\"https://git.msfjarvis.dev/\">https://git.msfjarvis.dev</a> for a while now, running <a href=\"https://about.gitea.com/\">Gitea</a> but my faith in the project’s open core model has steadily been going down. When Codeberg announced they were forking Gitea as <a href=\"https://forgejo.org\">Forgejo</a> I quietly put down a line item to switch over in my overflowing TODO list and promptly forgot about it.</p>\n<p>GitHub has been getting increasingly unpleasant for the past few years, but them officially announcing its demise as an independent entity and joining Microsoft’s “CoreAI” division was the straw that broke the camel’s back and made me want to use my own Git forge more actively. Thus the Forgejo migration shot up in my TODO list, and ended up being a rather involved process.</p>\n<blockquote>\n<p>This post is gonna have some rambling and going around in circles, but the TL;DR is that I ended up having to migrate individual accounts instead of the whole instance data by using a tool that I ironically generated using GitHub Copilot (the code is available <a href=\"https://git.msfjarvis.dev/msfjarvis/acceptable-vibes/src/branch/main/gitea-forgejo-migrator\">on my Git server</a>).</p>\n</blockquote>\n<h1 id=\"attempt-1-the-naive-way\">Attempt 1: The naive way</h1>\n<p>Forgejo initially used to support migrating data from Gitea pretty easily, but the increased friction of having to maintain a full fork against a moving target forced them to <a href=\"https://forgejo.org/2024-12-gitea-compatibility/\">drop support for these migration paths</a>. I of course am stupid and was not aware that I had missed my window, so I had to make the unpleasant discovery that simply swapping the latest Gitea for latest Forgejo will not work anymore. The <a href=\"https://nixos.org/manual/nixos/stable/#module-forgejo-migration-gitea\">NixOS manual’s migration section</a> suggested that this was possible, but it’s actually missing crucial information about version requirements that was <a href=\"https://github.com/NixOS/nixpkgs/commit/91947bb68e8184eba4c14476a6a14873f15e9ed4\">added later</a>.</p>\n<p>My naive attempt <em>almost</em> caused me to irreversibly corrupt my instance data, so I was more careful with future attempts and used a backup each time.</p>\n<h1 id=\"attempt-2-the-gitea-dump-command\">Attempt 2: The <code>gitea dump</code> command</h1>\n<p>This looked like it had some potential until I realized the lack of a companion <code>gitea restore</code> functionality. The generated dump is a big ZIP file of your entire state directory, along with the SQL commands to recreate your database with whatever database engine you were using. The recommended way to use this dump is to unzip it and then replay the SQL commands into <code>sqlite</code> (my current database) to build up the state directory. This approach kind of felt like a dead-end already but I gave it a good try anyway.</p>\n<p>I had also chosen to run Forgejo using PostgreSQL instead of SQLite, since I was already using it with a bunch of other software running on this machine and had proper backup strategies in place. This meant I had to reach out for <a href=\"https://pgloader.readthedocs.io/en/latest/\">pgloader</a> to import the SQLite-compatible dump into Postgres which worked out pretty well. Running <code>pgloader sqlite:///var/lib/gitea/data/gitea.db 'postgresql://forgejo:@/forgejo?host=/run/postgresql'</code> as root was able to successfully migrate the database to Postgres.</p>\n<p>Unfortunately this didn’t work due to the previously discussed version incompatibilities, and Forgejo rejected this database I had created for it. At this point I had lost a couple hours on this and constant electricity problems at my place had additionally caused my patience to run thin. I decided to cut my losses and re-deployed Forgejo with the same settings as my Gitea server, deciding that I would figure out a way to just copy my account data over since the instance configuration was managed by NixOS anyway (for the most part).</p>\n<h1 id=\"attempt-3-vibe-coding-deployed-somewhat-effectively\">Attempt 3: Vibe coding deployed somewhat effectively</h1>\n<blockquote>\n<p>I’m not generally a big believer in the AI hype. I have yet to pay for any of these tools, between GitHub and my employer I get plenty of access to top of the line models that are supposedly reinventing my field of work every 3 months. LLMs have yet to meaningfully help me at work, and the only times I’ve gotten real value out of them is by getting them to write one-off things like this that I am glad to have but would likely never invest the time to upskill into and build myself.</p>\n</blockquote>\n<p>I hooked up the free license to GitHub Copilot that I get for <a href=\"https://docs.github.com/en/copilot/get-started/plans\">satisfying some criteria of “popular open source maintainer”</a> into <a href=\"https://zed.dev\">Zed</a> and wrote out a simple README file describing what I wanted out of the tool and had it go to town. The end result of this was an unnecessarily abstracted Go project (I doubt anybody would use that directory structure for a project this small) that looked like it would do the job. You can find the source code for this <a href=\"https://git.msfjarvis.dev/msfjarvis/acceptable-vibes/src/branch/main/gitea-forgejo-migrator\">here</a>.</p>\n<blockquote>\n<p>According to <a href=\"https://github.com/msfjarvis/msfjarvis.dev/issues/80#issuecomment-3446970120\">a comment</a> on this post, organizations may not be migrated correctly by this script. I do not wish to change the code to an untested version so be prepared to do some manual fixes to address that deficiency. I would also accept a patch from someone who does end up fixing the script for themselves.</p>\n</blockquote>\n<p>Now onto actually running this tool. For this to work, it would require both my new Forgejo server and my old Gitea server to be up at the same time. First hurdle: conflicting ports. This was solved <a href=\"https://git.msfjarvis.dev/msfjarvis/dotfiles/commit/9a8cdd36cdf3f0b93834c86112fd113634985587\">pretty easily</a>.</p>\n<p>I screwed up here by running Forgejo on the primary domain and deploying the old Gitea server into a new Tailscale-based service. This caused multiple failures:</p>\n<h3 id=\"inability-to-log-into-the-gitea-instance\">Inability to log into the Gitea instance.</h3>\n<p>I could no longer sign into Gitea since I had 2FA enabled using Passkeys which require the domains to match up.</p>\n<p>To make up for the inability to log into my Gitea server to create an access token, I just used the gitea CLI instead.</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plain\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">sudo -i su - gitea</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\"># To get the actual path to the Gitea binary, which weirdly isn't installed for the gitea user?</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">systemctl cat gitea.service | grep ExecStart=</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">/nix/store/foo-bar-baz-gitea-1.23.1/bin/gitea admin -w $(pwd) user generate-access-token --token-name forgejo-migration --scopes \"read:repository,read:user\" --raw</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"sudo -i su - gitea# To get the actual path to the Gitea binary, which weirdly isn't installed for the gitea user?systemctl cat gitea.service | grep ExecStart=/nix/store/foo-bar-baz-gitea-1.23.1/bin/gitea admin -w $(pwd) user generate-access-token --token-name forgejo-migration --scopes &quot;read:repository,read:user&quot; --raw\"><div></div></button></div></figure></div>\n<p>On the forgejo side, a similar dance ensued but this time to actually create my account since this was a fresh start.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plain\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">sudo -i su - forgejo</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">systemctl cat forgejo.service | grep ExecStart=</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\"># yes the actual CLI seems to still be available as gitea</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">/nix/store/foo-bar-baz-forgejo-10.0.0/bin/gitea admin user create -w $(pwd) --username msfjarvis --email me@msfjarvis.dev --admin --access-token --access-token-name forgejo-migration --access-token-scopes \"read:user,write:repository\"</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"sudo -i su - forgejosystemctl cat forgejo.service | grep ExecStart=# yes the actual CLI seems to still be available as gitea/nix/store/foo-bar-baz-forgejo-10.0.0/bin/gitea admin user create -w $(pwd) --username msfjarvis --email me@msfjarvis.dev --admin --access-token --access-token-name forgejo-migration --access-token-scopes &quot;read:user,write:repository&quot;\"><div></div></button></div></figure></div>\n<p>With access tokens in hand, I ran the tool and hit my second problem.</p>\n<h3 id=\"tailscale-acls-bite-me-in-the-ass\">Tailscale ACLs bite me in the ass</h3>\n<p>The way Tailscale’s networking shebang works meant that the server running this isolated Gitea service couldn’t connect to it via the network without some tweaks to my ACL policies. I was not feeling like I had this extra debugging in me, so I opted to just temporarily hijack another subdomain pointing to this server for the purpose and resumed from there.</p>\n<h3 id=\"mirrors-of-github-private-repos-did-not-work\">Mirrors of GitHub private repos did not work</h3>\n<p>Back when I first created mirrors of all my stuff I had also done so for my private repos and just forgot about it. When the tool tried to migrate them they obviously failed to mirror since I wasn’t providing any access tokens for GitHub. I went back to Zed and had Claude add a retry for this and pull an access token from the <code>$GITHUB_TOKEN</code> environment variable.</p>\n<p>When I ran the tool again it was able to successfully recreate all the repositories from my Gitea server.</p>\n<h1 id=\"conclusion\">Conclusion</h1>\n<p>Overall this migration was worthwhile for me, both for documenting it for others in my position as well as for finally being free of Gitea. I trust the people in charge significantly more, and they continue to deliver <a href=\"https://forgejo.org/2025-07-release-v12-0/\">great work</a> driven in part by them actually using their own software at scale.</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/mildly-overengineering-my-glance-configuration/",
      "url": "https://msfjarvis.dev/posts/mildly-overengineering-my-glance-configuration/",
      "title": "Mildly overengineering my Glance configuration",
      "date_published": "2025-05-07T17:35:00.000Z",
      "summary": "The story of setting up a live environment for configuring my Glance dashboard",
      "content_html": "<blockquote>\n<p>May 10th update: Powered by a lack of sleep and extreme fatigue I cooked up a far simpler solution that is mentioned at the end of the post.</p>\n</blockquote>\n<h1 id=\"setting-the-scene\">Setting the scene</h1>\n<p>I’m a very happy user of the <a href=\"https://github.com/glanceapp/glance\">Glance</a> dashboard, and make use of it multiple times a day.</p>\n<p>As a NixOS user, I have a very popular problem of wanting to iterate on my Glance configuration without having to rebuild my whole system every time to see the change.</p>\n<p>One way I’ve seen this addressed (please message me on <code>@msfjarvis@fantastic.earth</code> if you can find the actual blog post) is to have some sort of “dev” switch that generates a writable version of your configuration using some <a href=\"https://github.com/nix-community/home-manager\">home-manager</a> tricks that you can poke at before encoding it into Nix. Turning off the aforementioned “dev” mode will make the configuration go through the more usual Nix ways and give you your declarative system back.</p>\n<p>I think that idea is cool! I also think that idea kinda sucks when the configuration is in a “language” you despise, like YAML. Which is where I am!</p>\n<h1 id=\"coming-up-with-a-solution\">Coming up with a solution</h1>\n<p>At the end of the day, the problem I have is that there are too many intermediate steps between me writing Nix and it being converted into YAML. So let’s break it down!</p>\n<p>Phase 1: Pull out the Nix bits I need to a standalone file that I can muck around with: <a href=\"https://msfjarvis.dev/g/dotfiles/39c90cb831c6\">done</a></p>\n<p>Now, we need a way to convert this Nix code into a YAML file that Glance can ingest. I came across a relatively new project that solves exactly this: <a href=\"https://github.com/theobori/nix-converter\">nix-converter</a>.</p>\n<p>Running <code>nix-converter --from-nix -f path/to/file.nix -l yaml</code> outputs YAML to stdout, ready to be shoved wherever you desire. This worked almost flawlessly, except an easily worked-around issue:</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"nix\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># This generates the following (incorrect) YAML</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># foo: \"baz\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">foo</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#1A57D1;--1:#FABD2F\">bar</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">baz</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># This way of declaraing the attrsets works around the</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># issue and generates the following:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># foo:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">#   bar: baz</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">foo</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#FABD2F\">bar</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">baz</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">};</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"{  # This generates the following (incorrect) YAML  # foo: &quot;baz&quot;  foo.bar = &quot;baz&quot;;  # This way of declaraing the attrsets works around the  # issue and generates the following:  # foo:  #   bar: baz  foo = {    bar = &quot;baz&quot;;  };}\"><div></div></button></div></figure></div>\n<p>Glance will automatically reload itself if the config file changes, which\ngreatly simplifies the setup. The only remaining bit we have is to automatically\ngenerate this YAML every time we make a change to the Nix file, so that we can freely\nswitch between editor and web browser to see changes without taking a trip to the\nterminal.</p>\n<p>I solved this using <a href=\"https://github.com/watchexec/watchexec\">watchexec</a> and a tiny Bash script that I stuffed\ninto a <a href=\"https://github.com/numtide/devshell\">devshell</a> command:</p>\n<div class=\"expressive-code\"><figure class=\"frame is-terminal\"><figcaption class=\"header\"><span class=\"title\"></span><span class=\"sr-only\">Terminal window</span></figcaption><pre data-language=\"bash\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#91AFA3\">out_conf</span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#5D5F6E;--1:#B4A694\">$(</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">mktemp</span><span style=\"--0:#5D5F6E;--1:#B4A694\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#B4A694\">${</span><span style=\"--0:#4C4F69;--1:#91AFA3\">lib</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">getExe</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">pkgs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">watchexec</span><span style=\"--0:#4C4F69;--1:#B4A694\">}</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> -r -w </span><span style=\"--1:#B4A694\"><span style=\"--0:#2C6E1E\">\"</span><span style=\"--0:#4C4F69\">${</span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">glanceConf</span><span style=\"--1:#B4A694\"><span style=\"--0:#4C4F69\">}</span><span style=\"--0:#2C6E1E\">\"</span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> -- </span><span style=\"--1:#B4A694\"><span style=\"--0:#2C6E1E\">\"</span><span style=\"--0:#4C4F69\">${</span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">lib</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">getExe</span><span style=\"--0:#2C6E1E;--1:#B8BB26\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">pkgs</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">nix-converter</span><span style=\"--0:#4C4F69;--1:#B4A694\">}</span><span style=\"--0:#2C6E1E;--1:#B8BB26\"> --from-nix -f </span><span style=\"--0:#4C4F69;--1:#B4A694\">${</span><span style=\"--0:#4C4F69;--1:#91AFA3\">glanceConf</span><span style=\"--0:#4C4F69;--1:#B4A694\">}</span><span style=\"--0:#2C6E1E;--1:#B8BB26\"> -l yaml &gt; </span><span style=\"--0:#4C4F69;--1:#91AFA3\">$out_conf</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">&amp;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#B4A694\">${</span><span style=\"--0:#4C4F69;--1:#91AFA3\">lib</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">getExe</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">pkgs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">glance</span><span style=\"--0:#4C4F69;--1:#B4A694\">}</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#D995A8\">-config</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#91AFA3\">$out_conf</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"out_conf=$(mktemp)${lib.getExe pkgs.watchexec} -r -w &quot;${glanceConf}&quot; -- &quot;${lib.getExe pkgs.nix-converter} --from-nix -f ${glanceConf} -l yaml > $out_conf&quot; &amp;${lib.getExe pkgs.glance} -config &quot;$out_conf&quot;\"><div></div></button></div></figure></div>\n<p>This is a mix of regular Bash and Nix which will make sense to Nix users, but\nfor everyone else: <code>${lib.getExe pkgs.watchexec}</code> gives you a path of the form\n<code>/nix/store/store-hash-watchexec-version/bin/watchexec</code> which is the binary you\ncan execute. <code>$glanceConf</code> is a Nix variable containing a string path to the\n<code>glance.nix</code> file we pulled out earlier.</p>\n<p>All put together it looks like\n<a href=\"https://msfjarvis.dev/g/dotfiles/e1bffa7e9d97\">this</a>. If I need to edit my\nGlance config, I will go into the <code>glance</code> devShell, boot up\n<a href=\"https://zed.dev\">Zed</a>, run the <code>dev</code> command in my terminal and enjoy a fully\nautomated iteration experience.</p>\n<h2 id=\"caveats\">Caveats</h2>\n<p>This setup is not even remotely perfect, <code>nix-generator</code> has bugs and discrepancies compared to the Nix to YAML implementation inside Nixpkgs. Not all constructs that work fine with Nixpkgs work that well with <code>nix-generator</code>, complicating development.</p>\n<h2 id=\"may-10th-update\">May 10th update</h2>\n<p>Out of the blue I had a minor crisis looking at the kludge I had previously put together and realized there was actually a simpler way to be generating this YAML code without having to forego the niceties of Nixpkgs. The end result of that is <a href=\"https://msfjarvis.dev/g/dotfiles/6d3eb0c849be\">this commit</a>, which I will explain below.</p>\n<p>The bulk of the work is now handled by a bash script, which I have annotated as comments below to explain what’s going on.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"bash\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#8D477A;--0fs:italic;--1:#B1A69C;--1fs:italic\">#!/usr/bin/env bash</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># EXPR is a Nix expression that we will be evaluating</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># and building to obtain our final YAML</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#91AFA3\">EXPR</span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">let</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">  </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\"># Load nixpkgs from the search path because it's the easiest way to do it.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">  </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">pkgs = import &lt;nixpkgs&gt; { };</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">  </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\"># This is the same code we use in our Glance module to generate the YAML</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">  </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">settingsFormat = pkgs.formats.yaml { };</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">  </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">settingsFile = settingsFormat.generate </span><span style=\"--0:#8D477A;--1:#D995A8\">\\\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">glance.yaml</span><span style=\"--0:#8D477A;--1:#D995A8\">\\\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\"> (import ./</span><span style=\"--0:#AD343E;--0fs:italic;--1:#91AFA3\">${1</span><span style=\"--0:#116B70;--1:#8EC07C\">:?</span><span style=\"--0:#AD343E;--0fs:italic;--1:#91AFA3\">}</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">);</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">in</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#2C6E1E;--1:#B8BB26\"># settingsFile returns a derivation that the </span><span style=\"--0:#116B70;--1:#B4A694\">`</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">nix</span><span style=\"--0:#2C6E1E;--1:#8EC07C\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">build</span><span style=\"--0:#116B70;--1:#B4A694\">`</span><span style=\"--0:#2C6E1E;--1:#B8BB26\"> below will, well, build.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#8D477A;--1:#D995A8\">\\\"\\$</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">{settingsFile}</span><span style=\"--0:#8D477A;--1:#D995A8\">\\\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">nix</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">build</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#D995A8\">\\</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Prevent Nix from trying to check binary caches each time we build</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">--option</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">substitute</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">false</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#D995A8\">\\</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Feed the nixpkgs input from your flakes registry to the search path</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">--impure</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#D995A8\">-I</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">nixpkgs=flake:nixpkgs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#D995A8\">\\</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Pass in our expression from above that will emit the derivation to be built</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">--expr</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--1:#B4A694\"><span style=\"--0:#2C6E1E\">\"</span><span style=\"--0:#4C4F69\">${</span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">EXPR</span><span style=\"--1:#B4A694\"><span style=\"--0:#4C4F69\">}</span><span style=\"--0:#2C6E1E\">\"</span></span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># ./result is the default nix3-build output path</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">glance</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#D995A8\">-config</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">./result</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"#!/usr/bin/env bash# EXPR is a Nix expression that we will be evaluating# and building to obtain our final YAMLEXPR=&quot;let  # Load nixpkgs from the search path because it's the easiest way to do it.  pkgs = import <nixpkgs> { };  # This is the same code we use in our Glance module to generate the YAML  settingsFormat = pkgs.formats.yaml { };  settingsFile = settingsFormat.generate \\&quot;glance.yaml\\&quot; (import ./${1:?});in# settingsFile returns a derivation that the `nix build` below will, well, build.\\&quot;\\${settingsFile}\\&quot;&quot;nix build \\  # Prevent Nix from trying to check binary caches each time we build  --option substitute false \\  # Feed the nixpkgs input from your flakes registry to the search path  --impure -I nixpkgs=flake:nixpkgs \\  # Pass in our expression from above that will emit the derivation to be built  --expr &quot;${EXPR}&quot;# ./result is the default nix3-build output pathglance -config ./result\"><div></div></button></div></figure></div>"
    },
    {
      "id": "https://msfjarvis.dev/posts/migrating-from-simple-analytics-to-self-hosted-plausible/",
      "url": "https://msfjarvis.dev/posts/migrating-from-simple-analytics-to-self-hosted-plausible/",
      "title": "Migrating from Simple Analytics to self-hosted Plausible",
      "date_published": "2025-04-22T11:13:00.000Z",
      "summary": "Documenting the misadventures of self-hosting my site analytics",
      "content_html": "<p>For the longest time (nearly 6 years according to the data export), I have been a user of <a href=\"https://www.simpleanalytics.com/\">Simple Analytics</a> and their dead simple web analytics offering. I believe I got access to it via GitHub’s big bundle of freemium services provided to students and they just didn’t bother checking that I had gone to and dropped out of college twice since they first offered me free access until this month.</p>\n<p>So while I was rather satisfied with the free ride I had gotten from Simple Analytics till now, having to pay a rather eye-watering 15 USD every month for it was stretching the otherwise $0 USD budget for my blog. The site itself is hosted by Netlify, but I have a small server from <a href=\"https://netcup.de\">netcup</a> which was suitable to take on the load of running a self-hosted analytics server that I could switch to.</p>\n<h1 id=\"choosing-the-analytics-service\">Choosing the analytics service</h1>\n<p>Like the absolute chump I am, my first visit was to <a href=\"https://awesome-selfhosted.net/tags/analytics.html\">awesome-selfhosted’s analytics page</a> and from there, the only options that stuck out were <a href=\"https://plausible.io\">Plausible</a> and <a href=\"https://matomo.org/\">Matomo</a>.</p>\n<p>I will be honest here, the reason to not choose Matomo was very superficial - I didn’t want to be running PHP and MySQL. Plausible is an Elixir app and thus not much better as far as me understanding the language goes, but I was reasonably more confident about its choice of Clickhouse and Postgres as the backing data stores.</p>\n<h1 id=\"getting-set-up\">Getting set up</h1>\n<p>My server runs NixOS, and thankfully there is a module for Plausible already so setup was very simple and all I had to add was this:</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"nix\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#91AFA3\">services</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">caddy</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">virtualHosts</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#E6928E\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">https://stats.msfjarvis.dev</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">let</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#FABD2F\">p</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">config</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">services</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">plausible</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">server</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">in</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#FABD2F\">extraConfig</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">''</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">      </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">reverse_proxy </span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">p</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">listenAddress</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">:</span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--1:#B8BB26\"><span style=\"--0:#1A57D1;--0fs:italic\">toString</span><span style=\"--0:#2C6E1E\"> </span></span><span style=\"--0:#4C5AA8;--1:#91AFA3\">p</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">port</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E\">    </span></span><span style=\"--0:#2C6E1E;--1:#B4A694\">''</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">};</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#4C4F69;--1:#E6928E\">;</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#91AFA3\">sops</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">secrets</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">plausible-secret</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#E6928E\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">sopsFile</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">lib</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">snowfall</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">fs</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">get-file</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">secrets/plausible.yaml</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#4C4F69;--1:#E6928E\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#91AFA3\">services</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">plausible</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#E6928E\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">enable</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--1:#D995A8\">true</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">server</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#FABD2F\">baseUrl</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">https://stats.msfjarvis.dev</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#FABD2F\">secretKeybaseFile</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">config</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">sops</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">secrets</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">plausible-secret</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">path</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">};</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#4C4F69;--1:#E6928E\">;</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"services.caddy.virtualHosts = {  &quot;https://stats.msfjarvis.dev&quot; = let    p = config.services.plausible.server;  in  {    extraConfig = ''      reverse_proxy ${p.listenAddress}:${toString p.port}    '';  };};sops.secrets.plausible-secret = {  sopsFile = lib.snowfall.fs.get-file &quot;secrets/plausible.yaml&quot;;};services.plausible = {  enable = true;  server = {    baseUrl = &quot;https://stats.msfjarvis.dev&quot;;    secretKeybaseFile = config.sops.secrets.plausible-secret.path;  };};\"><div></div></button></div></figure></div>\n<p>The only hiccup here was me apparently not knowing what the hell is 64 bytes supposed to mean and failing to fill in a long enough string into the <code>secretKeybaseFile</code>, which is how it came to end with <code>brother-man-please-work-what-the-fuck-man</code> during testing after which I swapped it out for a 20 word passphrase generated by Bitwarden.</p>\n<p>Once everything was running, I set up an admin account and replaced the SimpleAnalytics JS snippet on my website with the one Plausible gave me. Notable difference here was a lack of tracking for users who disable JS, so that’s a net improvement to user privacy that I was too naive to think about in 2020.</p>\n<h1 id=\"migrating-existing-data-from-simple-analytics\">Migrating existing data from Simple Analytics</h1>\n<p>To their credit, Simple Analytics made this very easy and the export option is at the top of their settings page. My only complaint is about their range selection feature, I found the calendar widget very un-intuitive and the input field didn’t let me just fill in the date manually.</p>\n<p>Once I had the 45 mB CSV in hand, I looked around for someone else’s homework to copy so I didn’t have to write the migration tool myself. I came across <a href=\"https://github.com/magiobus/simpleanalytics-to-plausible-converter\">this</a> repo which looked like it would do the job but I had no plans to install NodeJS, so I had gpt-4o (via my free, GitHub-provided Copilot license) rewrite it as a single-file Python script that I could hack on more comfortably. That version is <a href=\"https://git.msfjarvis.dev/msfjarvis/simpleanalytics-to-plausible\">available here</a>, with some additional changes described below.</p>\n<p>I ran the script and got a handful of CSVs that Plausible was happy to import, after which I realized that the location data was completely missing. Indeed, my hopes of not having to write any code were going to remain so.</p>\n<p>I pulled up the <a href=\"https://plausible.io/docs/csv-import#csv-format-guidelines\">Plausible CSV import docs</a>, got the headers it was expecting for the location CSV and was able to piece together the data relatively easily. One big omission was Simple Analytics not recording cities for page visits, which Plausible had a field for. After figuring out what it was expecting, I filled in zeroes for that column.</p>\n<blockquote>\n<p>I have a big gripe with Plausible’s non-existent error reporting here. When my newly generated CSV failed to import, Plausible simply put up an ⚠️ sign in the list of imports and offered no logs. After digging through their GitHub issues I discovered that the errors had to be <strong>pulled out from a Postgres table</strong> which I was not very impressed by. For others who come by here with the same problem: it’s in the <code>errors</code> column in the <code>oban_jobs</code> table of the <code>plausible</code> database.</p>\n</blockquote>\n<p>The final step was making the data public like it was on Simple Analytics, so my humble blog’s popularity or lack thereof can continue to be <a href=\"https://stats.msfjarvis.dev/msfjarvis.dev\">scrutinized by anyone</a>.</p>\n<h1 id=\"wrapping-up\">Wrapping up</h1>\n<p>All things considered this took me approximately an hour to get done, most of which was spent digging through Plausible’s GitHub issues and re-learning how to use the <code>psql</code> CLI. I’m quite pleased with how it turned out in the end!</p>\n<p>Thanks to <a href=\"https://mastodon.social/@tanvibhakta\">Tanvi</a> and <a href=\"https://fantastic.earth/@abnv\">Abhinav</a> for answering some questions regarding Plausible.</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/overengineering-an-obsidian-dashboard-to-get-better-at-marvel-snap/",
      "url": "https://msfjarvis.dev/posts/overengineering-an-obsidian-dashboard-to-get-better-at-marvel-snap/",
      "title": "Overengineering an Obsidian dashboard to get better at Marvel Snap",
      "date_published": "2025-04-01T05:16:52.000Z",
      "summary": "Using data to answer the ultimate gamer question - why am I so bad?",
      "content_html": "<p><a href=\"https://www.marvelsnap.com/\">Marvel Snap</a> at its core is a card battler set in the Marvel Universe, where you play a deck of 12 cards and are trying to win\nat least 2 of 3 randomly generated locations on the board in 6 turns.</p>\n<p>You start with 3 cards in your hand, and draw one each turn. Each card has an energy that decides what it costs to play, and a power that adds to the total\nto determine which player wins that location. You start at 1 energy and gain +1 energy every turn.</p>\n<p>The game has a competitive mode, and I am horrendous at it. Over the course of the 2 years or so I’ve played this game I’ve made multiple excuses as to why I am so unlucky on the ranked ladder but ultimately I just play the game wrong and then blame my opponents. Like a true gamer.</p>\n<p>Last month I <a href=\"https://msfjarvis.dev/posts/migrating-from-logseq-to-obsidian/\">migrated my personal journal and knowledge base</a> to <a href=\"https://obsidian.md\">Obsidian</a>. On a whim, I decided that it might be a fun project to build a system to track and visualize statistics about my Snap games to try and objectively answer the question - why am I so bad?</p>\n<h1 id=\"laying-down-the-foundation\">Laying down the foundation</h1>\n<p>To get started with analysis, I first need to figure out what data to collect and how.</p>\n<p>For a typical game, the stuff you want to record is the following:</p>\n<ul>\n<li>Your deck: The game has many archetypes, some more reliably effective than others</li>\n<li>Opponent’s deck: If they are playing a deck that hard counters or just tends to always beat yours</li>\n<li>Locations: Some locations benefit certain decks and can heavily influence the result</li>\n<li>Outcome: Whether you won, lost or retreated</li>\n<li>Cubes: You lose or gain cubes depending on the outcome and the number of players who “snapped”</li>\n</ul>\n<p>Taking all this into account I settled on the following template that I would use for each individual game I recorded.</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"markdown\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">---</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--1:#8EC07C;--1fw:bold\">date</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">date</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--1:#8EC07C;--1fw:bold\">time</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">time</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--1:#8EC07C;--1fw:bold\">type</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">snap-game</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--1:#8EC07C;--1fw:bold\">result</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">win/loss/retreat</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--1:#8EC07C;--1fw:bold\">opponent_name</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--1:#8EC07C;--1fw:bold\">opponent_deck</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--1:#8EC07C;--1fw:bold\">my_deck</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--1:#8EC07C;--1fw:bold\">locations</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">-</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">location1</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">-</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">location2</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">-</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">location3</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--1:#8EC07C;--1fw:bold\">cubes</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Use positive for gains (e.g., +4), negative for losses (e.g., -2)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--1:#8EC07C;--1fw:bold\">notes</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">---</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#A54107;--1:#B4A694;--1fw:bold\">##</span><span style=\"--0:#A54107;--1:#FE8929;--1fw:bold\"> Game Details</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#865511;--1:#B4A694;--1fw:bold\">###</span><span style=\"--0:#865511;--1:#FE8929;--1fw:bold\"> </span><span style=\"--0:#865511;--1:#FABD2F;--1fw:bold\">Key Moments</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#865511;--1:#B4A694;--1fw:bold\">###</span><span style=\"--0:#865511;--1:#FE8929;--1fw:bold\"> </span><span style=\"--0:#865511;--1:#FABD2F;--1fw:bold\">Lessons Learned</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"---date: { { date } }time: { { time } }type: snap-gameresult: win/loss/retreatopponent_name:opponent_deck:my_deck:locations:  - location1  - location2  - location3cubes: # Use positive for gains (e.g., +4), negative for losses (e.g., -2)notes:---## Game Details### Key Moments### Lessons Learned\"><div></div></button></div></figure></div>\n<p>The YAML frontmatter is what I use to power my dashboards, and the Markdown underneath is unstructured data to record how the game actually progressed so I can also have a play-by-play if I fumbled a good play or managed to cook something clever.</p>\n<p>Obsidian provides a very serviceable GUI to edit frontmatter fields which makes the job of filling it in pretty easy.</p>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/template.C6GyHpIt.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/template.C6GyHpIt.webp\" data-pswp-width=\"426\" data-pswp-height=\"486\" aria-label=\"Obsidian's GUI for the frontmatter given above, of note is the date picker and the 'chips' pattern for entering list values for the locations field\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/template.C6GyHpIt.webp\" alt=\"Obsidian's GUI for the frontmatter given above, of note is the date picker and the 'chips' pattern for entering list values for the locations field\" loading=\"lazy\" decoding=\"async\" width=\"426\" height=\"486\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a></figure>\n<p>I fed this page into the Obsidian <a href=\"https://help.obsidian.md/plugins/templates\">Templates</a> plugin which takes care of filling in the date and time automatically when I create a new page based on this template.</p>\n<h1 id=\"building-the-visualization\">Building the visualization</h1>\n<p>For this purpose I leveraged the excellent <a href=\"https://github.com/blacksmithgu/obsidian-dataview\">Obsidian DataView</a> plugin which provides an SQL-like query language that can introspect the Markdown documents in a standard Obsidian Vault and surface structured data from it.</p>\n<p>As an example, here’s the code for the dashboard that is in my daily note template to see the current day’s games.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"sql\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">TABLE</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">WITHOUT</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> ID</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">length</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">rows</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) </span><span style=\"--0:#7B34D9;--1:#FC8678\">as</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Total Games</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">length</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">filter</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">rows</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, (x) </span><span style=\"--0:#116B70;--1:#8EC07C\">=&gt;</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#D995A8\">x</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#4C4F69;--1:#D995A8\">result</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">win</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)) </span><span style=\"--0:#7B34D9;--1:#FC8678\">as</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> Wins,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">length</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">filter</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">rows</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, (x) </span><span style=\"--0:#116B70;--1:#8EC07C\">=&gt;</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#D995A8\">x</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#4C4F69;--1:#D995A8\">result</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">loss</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)) </span><span style=\"--0:#7B34D9;--1:#FC8678\">as</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> Losses,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">length</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">filter</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">rows</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, (x) </span><span style=\"--0:#116B70;--1:#8EC07C\">=&gt;</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#D995A8\">x</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#4C4F69;--1:#D995A8\">result</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">retreat</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)) </span><span style=\"--0:#7B34D9;--1:#FC8678\">as</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> Retreats,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--0:#1A57D1;--0fs:italic;--1:#EBDBB2\">sum</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(map(</span><span style=\"--0:#4C4F69;--1:#D995A8\">rows</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#4C4F69;--1:#D995A8\">cubes</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, (x) </span><span style=\"--0:#116B70;--1:#8EC07C\">=&gt;</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">number</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(x))) </span><span style=\"--0:#7B34D9;--1:#FC8678\">as</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Net Cubes</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">FROM</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">snap-games</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">WHERE</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">type</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">snap-game</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">AND</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">date</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">date</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">{{date}}</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">FLATTEN </span><span style=\"--0:#4C4F69;--1:#D995A8\">file</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#4C4F69;--1:#D995A8\">link</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">as</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> FileLink</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">GROUP BY</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">null</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"TABLE WITHOUT ID  length(rows) as &quot;Total Games&quot;,  length(filter(rows, (x) => x.result = &quot;win&quot;)) as Wins,  length(filter(rows, (x) => x.result = &quot;loss&quot;)) as Losses,  length(filter(rows, (x) => x.result = &quot;retreat&quot;)) as Retreats,  sum(map(rows.cubes, (x) => number(x))) as &quot;Net Cubes&quot;FROM &quot;snap-games&quot;WHERE type = &quot;snap-game&quot;  AND date = date(&quot;{{date}}&quot;)FLATTEN file.link as FileLinkGROUP BY null\"><div></div></button></div></figure></div>\n<p>It is pretty self-explanatory, but essentially this walks through every page in my Vault that has its <code>type</code> field set to <code>snap-game</code> and today’s date, then collates the win/loss record from the other YAML frontmatter fields and makes a nice and simple table out of it.</p>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/daily-dashboard.BEHc8Dqk.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/daily-dashboard.BEHc8Dqk.webp\" data-pswp-width=\"721\" data-pswp-height=\"78\" aria-label=\"A table with a single row showing the total games, the wins, the losses, the retreats and the net cube change for the day\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/daily-dashboard.BEHc8Dqk.webp\" alt=\"A table with a single row showing the total games, the wins, the losses, the retreats and the net cube change for the day\" loading=\"lazy\" decoding=\"async\" width=\"721\" height=\"78\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a></figure>\n<p>This can also be extended to cover the past week</p>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/weekly-dashboard.BM4a7o7V.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/weekly-dashboard.BM4a7o7V.webp\" data-pswp-width=\"721\" data-pswp-height=\"153\" aria-label=\"A table with a single row showing the total games, the wins, the losses, the retreats and the net cube change for the week\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/weekly-dashboard.BM4a7o7V.webp\" alt=\"A table with a single row showing the total games, the wins, the losses, the retreats and the net cube change for the week\" loading=\"lazy\" decoding=\"async\" width=\"721\" height=\"153\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a></figure>\n<details><summary>DataView code</summary><div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"sql\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">TABLE</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FE8929\">date</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">as</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">Date</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FE8929\">time</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">as</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">Time</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">result </span><span style=\"--0:#7B34D9;--1:#FC8678\">as</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> Result,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">my_deck </span><span style=\"--0:#7B34D9;--1:#FC8678\">as</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">My Deck</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">opponent_deck </span><span style=\"--0:#7B34D9;--1:#FC8678\">as</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Opponent's Deck</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">cubes </span><span style=\"--0:#7B34D9;--1:#FC8678\">as</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Cubes Δ</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">FROM</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">snap-games</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">WHERE</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">type</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">snap-game</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">AND</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">date</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">&gt;=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">date</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(today) </span><span style=\"--0:#116B70;--1:#8EC07C\">-</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> dur(</span><span style=\"--0:#A54107;--1:#D995A8\">7</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">days</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">SORT </span><span style=\"--0:#7B34D9;--1:#FE8929\">date</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">desc</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, </span><span style=\"--0:#7B34D9;--1:#FE8929\">time</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">desc</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"TABLE  date as Date,  time as Time,  result as Result,  my_deck as &quot;My Deck&quot;,  opponent_deck as &quot;Opponent's Deck&quot;,  cubes as &quot;Cubes Δ&quot;FROM &quot;snap-games&quot;WHERE type = &quot;snap-game&quot;  AND date >= date(today) - dur(7 days)SORT date desc, time desc\"><div></div></button></div></figure></div></details>\n<hr>\n<p>I can see what decks I played and how they performed relative to each other</p>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/deck-performance.DaVMbHIJ.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/deck-performance.DaVMbHIJ.webp\" data-pswp-width=\"721\" data-pswp-height=\"188\" aria-label=\"A table showing the decks I played in each row and their respective game counts, win rate and net cubes statistics\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/deck-performance.DaVMbHIJ.webp\" alt=\"A table showing the decks I played in each row and their respective game counts, win rate and net cubes statistics\" loading=\"lazy\" decoding=\"async\" width=\"721\" height=\"188\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a></figure>\n<details><summary>DataView code</summary><div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"sql\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">TABLE</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">length</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">rows</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) </span><span style=\"--0:#7B34D9;--1:#FC8678\">as</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> Games,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--0:#1A57D1;--0fs:italic;--1:#EBDBB2\">round</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">length</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">filter</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">rows</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, (x) </span><span style=\"--0:#116B70;--1:#8EC07C\">=&gt;</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#D995A8\">x</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#4C4F69;--1:#D995A8\">result</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">win</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)) </span><span style=\"--0:#116B70;--1:#8EC07C\">/</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">length</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">rows</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) </span><span style=\"--0:#116B70;--1:#8EC07C\">*</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">100</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, </span><span style=\"--0:#A54107;--1:#D995A8\">1</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) </span><span style=\"--0:#116B70;--1:#8EC07C\">+</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">%</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">as</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Win Rate</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--0:#1A57D1;--0fs:italic;--1:#EBDBB2\">sum</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(map(</span><span style=\"--0:#4C4F69;--1:#D995A8\">rows</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#4C4F69;--1:#D995A8\">cubes</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, (x) </span><span style=\"--0:#116B70;--1:#8EC07C\">=&gt;</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">number</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(x))) </span><span style=\"--0:#7B34D9;--1:#FC8678\">as</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Net Cubes</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">FROM</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">snap-games</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">WHERE</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">type</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">snap-game</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">AND</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">date</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">&gt;=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">date</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(today) </span><span style=\"--0:#116B70;--1:#8EC07C\">-</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> dur(</span><span style=\"--0:#A54107;--1:#D995A8\">7</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">days</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">GROUP BY</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> my_deck</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">SORT </span><span style=\"--0:#7B34D9;--1:#FC8678\">length</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">rows</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) </span><span style=\"--0:#7B34D9;--1:#FC8678\">desc</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"TABLE  length(rows) as Games,  round(length(filter(rows, (x) => x.result = &quot;win&quot;)) / length(rows) * 100, 1) + &quot;%&quot; as &quot;Win Rate&quot;,  sum(map(rows.cubes, (x) => number(x))) as &quot;Net Cubes&quot;FROM &quot;snap-games&quot;WHERE type = &quot;snap-game&quot;  AND date >= date(today) - dur(7 days)GROUP BY my_deckSORT length(rows) desc\"><div></div></button></div></figure></div></details>\n<hr>\n<p>I can also see which locations favor me the most</p>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/locations.DcrRl9TP.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/locations.DcrRl9TP.webp\" data-pswp-width=\"721\" data-pswp-height=\"888\" aria-label=\"A table of every location with its name, number of times I played there, my win rate and the net cubes\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/locations.DcrRl9TP.webp\" alt=\"A table of every location with its name, number of times I played there, my win rate and the net cubes\" loading=\"lazy\" decoding=\"async\" width=\"721\" height=\"888\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a></figure>\n<details><summary>DataView code</summary><div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"sql\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">TABLE</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">length</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">rows</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) </span><span style=\"--0:#7B34D9;--1:#FC8678\">as</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Times Encountered</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--0:#1A57D1;--0fs:italic;--1:#EBDBB2\">round</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">length</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">filter</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">rows</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, (x) </span><span style=\"--0:#116B70;--1:#8EC07C\">=&gt;</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#D995A8\">x</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#4C4F69;--1:#D995A8\">result</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">win</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)) </span><span style=\"--0:#116B70;--1:#8EC07C\">/</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">length</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">rows</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) </span><span style=\"--0:#116B70;--1:#8EC07C\">*</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">100</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, </span><span style=\"--0:#A54107;--1:#D995A8\">1</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) </span><span style=\"--0:#116B70;--1:#8EC07C\">+</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">%</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">as</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Win Rate</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--0:#1A57D1;--0fs:italic;--1:#EBDBB2\">sum</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(map(</span><span style=\"--0:#4C4F69;--1:#D995A8\">rows</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#4C4F69;--1:#D995A8\">cubes</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, (x) </span><span style=\"--0:#116B70;--1:#8EC07C\">=&gt;</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">number</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(x))) </span><span style=\"--0:#7B34D9;--1:#FC8678\">as</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Net Cubes</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">FROM</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">snap-games</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">WHERE</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">type</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">snap-game</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">AND</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">date</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">&gt;=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">date</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(today) </span><span style=\"--0:#116B70;--1:#8EC07C\">-</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> dur(</span><span style=\"--0:#A54107;--1:#D995A8\">7</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">days</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">FLATTEN locations</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">GROUP BY</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> locations</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--1:#EBDBB2\"><span style=\"--0:#4C4F69\">SORT </span><span style=\"--0:#1A57D1;--0fs:italic\">sum</span><span style=\"--0:#4C4F69\">(map(</span></span><span style=\"--0:#4C4F69;--1:#D995A8\">rows</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#4C4F69;--1:#D995A8\">cubes</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, (x) </span><span style=\"--0:#116B70;--1:#8EC07C\">=&gt;</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">number</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(x))) </span><span style=\"--0:#7B34D9;--1:#FC8678\">desc</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"TABLE  length(rows) as &quot;Times Encountered&quot;,  round(length(filter(rows, (x) => x.result = &quot;win&quot;)) / length(rows) * 100, 1) + &quot;%&quot; as &quot;Win Rate&quot;,  sum(map(rows.cubes, (x) => number(x))) as &quot;Net Cubes&quot;FROM &quot;snap-games&quot;WHERE type = &quot;snap-game&quot;  AND date >= date(today) - dur(7 days)FLATTEN locationsGROUP BY locationsSORT sum(map(rows.cubes, (x) => number(x))) desc\"><div></div></button></div></figure></div></details>\n<h1 id=\"did-it-help\">Did it help?</h1>\n<p>I don’t know!</p>\n<p>Work getting hectic meant I had little time to actually play and the re-launch of the <a href=\"https://www.marvelsnap.com/newsdetail?id=7415247892945885957\">High Voltage</a> mode took up most of that.</p>\n<p>I did play a tiny amount of games when I first built this out, and did pick up on some of my tendencies that make me lose games but ultimately it’s a very small sample size. The new season of Snap will start tomorrow so hopefully I’ll be able to get back into it and actually figure out if this whole shebang was worth it.</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/the-obsidian-migration--one-week-later/",
      "url": "https://msfjarvis.dev/posts/the-obsidian-migration--one-week-later/",
      "title": "The Obsidian Migration - One Week Later",
      "date_published": "2025-03-17T05:38:37.000Z",
      "summary": "An experience report of using Obsidian for a week, coming from a year of Logseq",
      "content_html": "<p>In my <a href=\"https://msfjarvis.dev/posts/migrating-from-logseq-to-obsidian\">last post</a> I had a list of to-dos to fully finish up the migration, which I’ll reproduce below:</p>\n<blockquote>\n<ul>\n<li>I used internal links of <code>[[this format]]</code> pretty liberally in my Logseq graph as a tag system, which will need migration to Obsidian’s tags mechanism.</li>\n<li>Some of my pages refer to daily journal pages using a humanized date variant which Logseq automatically converted to proper links, which does not work in Obsidian.</li>\n<li>There seems to be no automatic tagger for Obsidian similar to how <a href=\"https://github.com/sawhney17/logseq-automatic-linker\">logseq-automatic-linker</a> works.</li>\n</ul>\n</blockquote>\n<h2 id=\"migrating-dates-and-tags-to-obsidian\">Migrating dates and tags to Obsidian</h2>\n<p>I haven’t found a solution for automated tagging yet, but inspired by Simon Willison’s <a href=\"https://simonwillison.net/2025/Mar/11/using-llms-for-code/\">post about how he uses LLMs for code</a> I decided to have GitHub Copilot write out a Python script to perform the migration of Logseq-isms to the appropriate Obsidian-native patterns.</p>\n<p>The choice of Python was for multiple reasons: it’s fast enough for the job, I understand it well, and it can get much farther without needing third-party dependencies.</p>\n<p>There wasn’t admittedly a whole lot to this process, since all I was doing is reviewing and running some 100 odd lines of Python then hard resetting my repo after it did the wrong thing. My initial prompt was this:</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plaintext\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">Write a Python script using only the standard library to process an Obsidian</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">vault and migrate Logseq patterns to Obsidian's systems in the markdown files.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">Below is a list of changes to make:</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">1. Convert any `[[Thing]]` style links to use Obsidian-native tags by adding a</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">`#` prefix, resulting in `#Thing`. If there are spaces in the link text,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">convert them to underscores.</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">2. If a link's text matches the date format `May 8th, 2024`, replace its text</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">by changing the date format to YYYY-MM-DD. In the given example, the output</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">should be `[[2024-05-08]]`.</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"Write a Python script using only the standard library to process an Obsidianvault and migrate Logseq patterns to Obsidian's systems in the markdown files.Below is a list of changes to make:1. Convert any `[[Thing]]` style links to use Obsidian-native tags by adding a`#` prefix, resulting in `#Thing`. If there are spaces in the link text,convert them to underscores.2. If a link's text matches the date format `May 8th, 2024`, replace its textby changing the date format to YYYY-MM-DD. In the given example, the outputshould be `[[2024-05-08]]`.\"><div></div></button></div></figure></div>\n<p>The first output from this was unsurprisingly buggy, the script converted <code>[[May 13th, 2024]]</code> to <code>#2024-05-13</code> instead of <code>[[2024-05-13]]</code>. The second iteration did not resolve the bug either, but in the third one progress was made.</p>\n<p>This was where Copilot impressed me a bit. After running the third iteration and finding a bug in the results, I told the chatbot that the date <code>Jun 8th, 2024</code> was not being converted and it was able to quickly clock that this was caused by my usage of <code>May</code> in the initial prompt which made it look only for full month names, while <code>Jun</code> is obviously abbreviated. I’m not sure if I would’ve figured that one out right away, but it was a good way to also learn more about the Python standard library’s date formatting facilities.</p>\n<p>The final script is available <a href=\"https://gist.github.com/msfjarvis/1892c898df746cfa1d24932a02a1da82\">here</a>, with individual revisions so you can step through the changes made by Copilot as I prompted it.</p>\n<h2 id=\"getting-set-up-on-mobile\">Getting set up on mobile</h2>\n<p>This was the bit I was more excited for, because one major advantage of using Obsidian on mobile that I didn’t realize earlier was that the same plugins Just Work™️. As far as I could tell, Logseq did not have support for plugins on mobile which mas a little annoying because how I interacted with the app changed across platforms even though there was no technical reason for it to. In Obsidian everything works out-of-the-box since plugins get vendored into your vault so you can actually modify and review the plugin code you’re running.</p>\n<p>My existing workflow on Android involved manually committing and pushing changes to my graph/vault via <a href=\"https://github.com/maks/MGit\">MGit</a> which was too many clicks in the MGit app and was getting a little frustrating over time. I <a href=\"https://masto-archive.msfjarvis.dev/@msfjarvis/114136599118536735\">asked on the Fediverse</a> if someone was aware of a newer Git client for Android and very quickly discovered <a href=\"https://github.com/catpuppyapp/PuppyGit\">PuppyGit</a>.</p>\n<p>Once I had removed Logseq and imported my existing vault into Obsidian (I’ve opted to re-use my existing Git repository), I followed PuppyGit’s guide to <a href=\"https://www.patreon.com/posts/puppygit-auto-122757321\">setting up automatic syncing with Obsidian</a> which worked flawlessly. It works by mapping a list of apps to a list of repositories that they interact with, and using an accessibility service monitors when apps are launched or closed, upon which it would pull remote changes to the repos or commit and push respectively.</p>\n<p>The Git plugin I was using for my desktop didn’t work on Android since it required a <code>git</code> binary, but they helpfully include an option to disable it only for the current device which let me delegate to PuppyGit on mobile with no issues.</p>\n<hr>\n<p>A week into it I find myself much happier using Obsidian than I ever was with Logseq, and I’ve even started using it for more things beyond journaling such as taking meeting notes and organizing my tasks which I found too unintuitive to achieve with Logseq. Logseq excels at a daily journaling tool but once my needs started expanding it was pretty clear that Obsidian really is the all-rounder I needed.</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/migrating-from-logseq-to-obsidian/",
      "url": "https://msfjarvis.dev/posts/migrating-from-logseq-to-obsidian/",
      "title": "Migrating from Logseq to Obsidian",
      "date_published": "2025-03-09T10:29:00.000Z",
      "summary": "Logseq's clunky apps and glacial development pace finally motivated me to migrate my journaling to Obsidian, here's how I did it.",
      "content_html": "<p>I have been using <a href=\"https://logseq.com\">Logseq</a> for just over <a href=\"https://masto-archive.msfjarvis.dev/@msfjarvis/112378523734491769\">a year</a> to maintain a daily journal, but I’ve been eyeing Obsidian for the last couple months as my note-keeping needs have expanded past daily journaling.</p>\n<p>Logseq has had a long-standing problem with <a href=\"https://github.com/logseq/logseq/issues/11644\">the version of Electron used by it</a> being EOL, which finally prompted its removal from <a href=\"https://github.com/NixOS/nixpkgs\">Nixpkgs</a> on security grounds this week and accelerating my migration to Obsidian. The below is a short list of things I did to make Obsidian more comfortable as a daily journaling system.</p>\n<h2 id=\"moving-over-content\">Moving over content</h2>\n<p>I made the following changes to the files in my Logseq graph when moving to the new Obsidian Vault</p>\n<ul>\n<li><code>assets</code> and <code>pages</code> got copied over as-is</li>\n<li>The <code>journals</code> folder got renamed to <code>Daily</code> since I wanna do notes for longer durations as well, and all the files inside renamed to replace underscores with hyphens since it looked like that was more common in Obsidian.</li>\n</ul>\n<h2 id=\"plugins\">Plugins</h2>\n<p>I enabled the following core plugins:</p>\n<ul>\n<li><strong>Daily Notes</strong>: Automates creation of daily notes</li>\n<li><strong>Templates</strong>: Allows you to create reusable templates</li>\n<li><strong>Backlinks</strong>: Shows references to the current note</li>\n</ul>\n<p>Looking around other people’s experience with this migration path I also found the following community plugins:</p>\n<ul>\n<li><strong>Periodic Notes</strong>: Enhanced daily/weekly/monthly notes</li>\n<li><strong>Natural Language Dates</strong>: Type dates like “tomorrow” or “next Monday”</li>\n<li><strong>Tasks</strong>: Advanced task management</li>\n<li><strong>Dataview</strong>: Query and display info from your notes</li>\n<li><strong>Templater</strong>: More powerful templating than the core plugin</li>\n<li><strong>Git</strong>: Self-managed sync for vaults</li>\n</ul>\n<blockquote>\n<p>Side note: I greatly appreciate the fact that Obsidian defaults to vendoring plugins in the Vault itself so if you’re tracking the <code>.obsidian</code> folder in Git, you get a fully reproducible Vault regardless of what machine you open it on.</p>\n</blockquote>\n<h2 id=\"configuration-changes\">Configuration changes</h2>\n<p>In Settings → Daily Notes:</p>\n<ul>\n<li><strong>Date format</strong>: <code>YYYY-MM-DD</code></li>\n<li><strong>New file location</strong>: <code>Daily/</code></li>\n<li><strong>Open daily note on startup</strong>: <code>Enabled</code></li>\n</ul>\n<p>In Settings → Hotkeys:</p>\n<ul>\n<li>Open today’s daily note: <code>Alt+D</code></li>\n<li>Create new note: <code>Ctrl+N</code></li>\n<li>Toggle edit/preview mode: <code>Ctrl+E</code></li>\n<li>Search in all notes: <code>Ctrl+Shift+F</code></li>\n</ul>\n<h1 id=\"pending-changes\">Pending changes</h1>\n<p>What I’ve done so far is just the result of 30 minutes of looking around, and there is still a bunch of currently broken stuff that needs resolving.</p>\n<ul>\n<li>\n<p>I used internal links of <code>[[this format]]</code> pretty liberally in my Logseq graph as a tag system, which will need migration to Obsidian’s tags mechanism.</p>\n</li>\n<li>\n<p>Some of my pages refer to daily journal pages using a humanized date variant which Logseq automatically converted to proper links, which does not work in Obsidian.</p>\n</li>\n<li>\n<p>There seems to be no automatic tagger for Obsidian similar to how <a href=\"https://github.com/sawhney17/logseq-automatic-linker\">logseq-automatic-linker</a> works.</p>\n</li>\n</ul>\n<p>Despite these issues, I think the move to Obsidian will end up being a net positive to my note taking process. Please let me know on <a href=\"https://masto-archive.msfjarvis.dev/@msfjarvis/114132263177269990\">Mastodon</a> or the comments here if you have any tips to improve things.</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/assorted-nixos-things/",
      "url": "https://msfjarvis.dev/posts/assorted-nixos-things/",
      "title": "Assorted NixOS things",
      "date_published": "2024-12-07T20:19:00.000Z",
      "summary": "Running log of random things I've learned about Nix/NixOS/Nixpkgs",
      "content_html": "<p>I run NixOS on my desktop and servers which results in coming across a bunch of generally random things, this is a running log of those discoveries which I’ll attempt to keep up-to-date with my personal Logseq graph where I’ve kept the record of them.</p>\n<h1 id=\"nix3-flake-metadata\">nix3-flake-metadata</h1>\n<p>The “new” (read: experimental) Nix CLI has a bunch of useful commands one of which is <code>nix flake metadata</code> that dumps a bunch of information about a flake, most notably, a tree-like representation of your inputs. It makes it significantly easier to find duplicate inputs and avoid the problem of <a href=\"https://zimbatm.com/notes/1000-instances-of-nixpkgs\">“1000 instances of Nixpkgs”</a>. <a href=\"https://github.com/nix-community/nix-melt\">nix-melt</a> can also assist with this, though for this purpose I find the output from the Nix CLI to be easier to grok than navigating the nix-melt TUI.</p>\n<h1 id=\"nixos-module-system\">NixOS module system</h1>\n<p>The module system is behind a lot of what makes NixOS and Nixpkgs so powerful for composing reproducible systems. The nix.dev reference includes a <a href=\"https://nix.dev/tutorials/module-system/index.html\">great tutorial</a> that is a great read regardless of the complexity level of what you are trying to achieve.</p>\n<h1 id=\"updating-your-custom-packages-from-a-git-branch\">Updating your custom packages from a Git branch</h1>\n<blockquote>\n<p>This isn’t directly related to Nix itself but to <a href=\"https://github.com/mic92/nix-update\">nix-update</a> which is a very popular tool for keeping Nix packages up-to-date’</p>\n</blockquote>\n<p>You can pass the <code>--version=branch</code> argument to update the package from the default branch instead of the latest tag/release for an Arch-style <code>-git</code> package. Specify <code>--version=branch=branch_name</code> if you want to use a non-default branch instead.</p>\n<h1 id=\"the-nix-language\">The Nix language</h1>\n<p>It is possible to replace a call to <code>builtins.map</code> with <code>nixpkgs.lib.catAttrs</code> if the transformation is just pulling out a field. For example: <code>builtins.map (item: item.field) list</code> can instead just be <code>catAttrs \"field\" list</code>.</p>\n<h1 id=\"building-packages-from-a-repl\">Building packages from a REPL</h1>\n<p>In a Nix REPL you can run <code>:bl inputs.nixpkgs.legacyPackages.aarch64-linux.pkgs.attic-server</code> to build the derivation and create a <code>repl-result-out</code> symlink in your current working directory. You can also run <code>:bl nixosConfigurations.ryzenbox.pkgs.attic-client</code> if you wish to build the package from your NixOS configuration’s instance of Nixpkgs.</p>\n<h1 id=\"avoiding-mysterious-errors-with-fixed-output-derivations\">Avoiding mysterious errors with fixed-output derivations</h1>\n<p>For many languages Nixpkgs’ builders vendor dependencies in a fixed-output derivation (FOD) which will give seemingly random errors if the contents change but its name doesn’t, since the old copy of the FOD will continue to be used in the later build stages. For example, in a package using <code>buildGoModule</code>, updating the version but not changing the <code>vendorHash</code> will cause a new FOD to be created but the build will continue with the FOD of the previous version.</p>\n<p>To avoid these problems when doing manual updates, make sure to change the relevant <code>cargoHash</code>/<code>vendorHash</code> to <code>lib.fakeHash</code> to cause the new FOD to be picked up which will also give you the correct hash to fill in the field.</p>\n<h1 id=\"tailscale-exit-nodes-on-nixos\">Tailscale exit nodes on NixOS</h1>\n<p><a href=\"https://tailscale.com\">Tailscale</a> is a mesh VPN that includes a feature to run certain endpoints as <a href=\"https://tailscale.com/kb/1103/exit-nodes\">exit nodes</a>, allowing traffic from other devices in the VPN to be routed from that machine.</p>\n<p>On NixOS, the relevant firewall knobs for it are exposed under the <a href=\"https://search.nixos.org/options?channel=24.11&amp;show=services.tailscale.useRoutingFeatures&amp;from=0&amp;size=50&amp;sort=relevance&amp;type=packages&amp;query=services.tailscale.useRoutingFeatures\"><code>services.tailscale.useRoutingFeatures</code></a> option which is required to run an exit node on a machine as well as allow a machine to use a different exit node. Set this to <code>server</code> if you want to run the specific machine as an exit node, and to <code>client</code> if you want it to be able to use exit nodes in your tailnet.</p>\n<h1 id=\"dual-booting-windows-using-systemd-boot\">Dual booting Windows using systemd-boot</h1>\n<p>NixOS includes options within its systemd-boot module that allow configuring boot entries for Windows without requiring manual intervention of copying over EFIs and what not. There are an initial few steps involved to get the relevant values, which are documented <a href=\"https://search.nixos.org/options?channel=24.11&amp;show=boot.loader.systemd-boot.windows&amp;from=0&amp;size=50&amp;sort=relevance&amp;type=packages&amp;query=boot.loader.systemd-boot.windows\">in the option reference</a>.</p>\n<h1 id=\"nixs-builtinstostring\">Nix’s <code>builtins.toString</code></h1>\n<p>The <code>toString</code> builtin in Nix handles Booleans rather unintuitively (even if it makes sense for the shell interpolation use-case) so here’s the <a href=\"https://nix.dev/manual/nix/2.24/language/builtins.html#builtins-toString\">docs</a> for you to go read just in case something else is a surprise.</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/deploying-applications-to-flyio-without-docker/",
      "url": "https://msfjarvis.dev/posts/deploying-applications-to-flyio-without-docker/",
      "title": "Deploying applications to Fly.io without Docker",
      "date_published": "2024-10-05T06:30:00.000Z",
      "summary": "A quick guide to deploying apps to Fly.io without a local Docker installation",
      "content_html": "<blockquote>\n<p>This post assumes basic familiarity with Fly.io, the flyctl CLI and Nix.</p>\n</blockquote>\n<hr>\n<p><a href=\"https://fly.io\">Fly.io</a> is one of the many players in the “We run your app close to your users” space. I’ve used their services for just over a couple years to run my personal instance of <a href=\"https://github.com/msfjarvis/linkleaner\">linkleaner</a>. Recently I ran into a combination of a platform outage and localized issue that necessitated recreating the app from scratch, and I decided to try doing so locally rather than my usual CI-based deployment system.</p>\n<p>Deployments with Fly.io usually start with a Docker/OCI image. Fly.io requires this image to either exist in their own OCI registry at registry.fly.io, or any other publicly accessible registry. The common way to do this is to use a Dockerfile and build the image locally, then use their <a href=\"https://github.com/superfly/flyctl\">flyctl</a> CLI to upload that image from your local Docker registry to registry.fly.io and then deploy the app.</p>\n<p>Since I use <a href=\"https://nixos.org\">Nix</a> as my Docker image builder (check out <a href=\"https://xeiaso.net/talks/2024/nix-docker-build/\">this talk</a> that explains why one would prefer it over Docker itself), I do not run Docker locally and had no plans to start now. This guide is mostly the result of that stubbornness so I intend to keep it short.</p>\n<h2 id=\"setting-up\">Setting up</h2>\n<p>The main ingredient of this is the <a href=\"https://github.com/containers/skopeo\">skopeo</a> CLI that allows one to interact with remote OCI registries. We’ll replace the Fly.io Docker integration steps with <a href=\"https://github.com/containers/skopeo/blob/03ca12ed56db3e5e46805b0d8a95a1207a0921ea/docs/skopeo-copy.1.md\">skopeo copy</a> that allows copying Docker archives directly to a registry.</p>\n<p>You’re also going to need a Fly.io token to authenticate with registry.fly.io. With <code>flyctl</code> installed you can run <code>fly tokens create deploy</code> to get an auth token usable for the purpose. I stored mine in a <code>FLY_AUTH_TOKEN</code> environment variable that will be referenced later.</p>\n<h2 id=\"building-and-deploying\">Building and deploying</h2>\n<p>First we need to build the image. With Nix it is as easy as running <code>nix build .#container</code> (since I declare the OCI image as a <a href=\"https://github.com/msfjarvis/linkleaner/blob/69564ab458abe77e55d090d29ff970a68c1a985c/flake.nix#L83-L93\">package</a> in my Nix configuration) which will generate a symlink at <code>./result</code> pointing to a Docker image tarball.</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame is-terminal\"><figcaption class=\"header\"><span class=\"title\"></span><span class=\"sr-only\">Terminal window</span></figcaption><pre data-language=\"bash\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">➜</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">ls</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#D995A8\">-l</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">result</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">result</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">⇒</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">/nix/store/h0jgv00fg68d7gsaadd11ydpk6wxxn8w-docker-image-linkleaner.tar.gz</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"➜ ls -l resultresult ⇒ /nix/store/h0jgv00fg68d7gsaadd11ydpk6wxxn8w-docker-image-linkleaner.tar.gz\"><div></div></button></div></figure></div>\n<p>Now to the actual deployment: assuming your app is already set up (<code>fly apps create</code>), you just need to copy this image over to their registry and trigger a new deployment. <code>skopeo</code> makes this rather easy.</p>\n<div class=\"expressive-code\"><figure class=\"frame is-terminal\"><figcaption class=\"header\"><span class=\"title\"></span><span class=\"sr-only\">Terminal window</span></figcaption><pre data-language=\"bash\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">skopeo</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#D995A8\">\\</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Remove the need for a policy file since we only need this for a one off thing</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">--insecure-policy</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#D995A8\">\\</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">copy</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#D995A8\">\\</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># The source argument, which is a docker image archive at `./result`</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">docker-archive:./result</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#D995A8\">\\</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># The destination argument, the `build.image` value from your fly.toml</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">docker://registry.fly.io/linkleaner:latest</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#D995A8\">\\</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Injecting the previously generated credentials</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">--dest-creds</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">x:</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#91AFA3\">$FLY_AUTH_TOKEN</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#D995A8\">\\</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Docker v2.2 manifest format: https://containers.gitbook.io/build-containers-the-hard-way#registry-format-oci-image-manifest</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Probably not necessary but I just wanted to be sure</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">--format</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">v2s2</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"skopeo \\  --insecure-policy \\  copy \\  docker-archive:./result \\  docker://registry.fly.io/linkleaner:latest \\  --dest-creds x:&quot;$FLY_AUTH_TOKEN&quot; \\  --format v2s2\"><div></div></button></div></figure></div>\n<p>This will print a bunch of stuff while it’s doing its thing and upon success you have now placed your organically built, Docker-free OCI image into the Fly.io registry.</p>\n<hr>\n<p>And that’s more or less it! You can now proceed with the usual Fly.io deployment process of running <code>flyctl deploy</code> and it’ll be able to find the image and run your application:</p>\n<div class=\"expressive-code\"><figure class=\"frame is-terminal\"><figcaption class=\"header\"><span class=\"title\"></span><span class=\"sr-only\">Terminal window</span></figcaption><pre data-language=\"bash\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">➜</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">flyctl</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">deploy</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">==</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">&gt; </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Verifying</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">app</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">config</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">Validating</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">/home/msfjarvis/code/linkleaner/fly.toml</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">✓</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Configuration</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">is</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">valid</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">--</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">&gt; </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Verified</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">app</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">config</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">==</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">&gt; </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Building</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">image</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">Searching</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">for</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">image</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">'</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">registry.fly.io/linkleaner:latest</span><span style=\"--0:#2C6E1E;--1:#B4A694\">'</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">remotely...</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">image</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">found:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">img_3mno4wk809624k18</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">Watch</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">your</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">deployment</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">at</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">https://fly.io/apps/linkleaner/monitoring</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">-------</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">Updating</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">existing</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">machines</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">in</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">'</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">linkleaner</span><span style=\"--0:#2C6E1E;--1:#B4A694\">'</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">with</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">rolling</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">strategy</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">-------</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">✔</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> [1/2] Cleared lease </span><span style=\"--0:#7B34D9;--1:#FC8678\">for</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> 0801937b607358</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">✔</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> [2/2] Cleared lease </span><span style=\"--0:#7B34D9;--1:#FC8678\">for</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> 9080155dfe0de8</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">-------</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"➜ flyctl deploy==> Verifying app configValidating /home/msfjarvis/code/linkleaner/fly.toml✓ Configuration is valid--> Verified app config==> Building imageSearching for image 'registry.fly.io/linkleaner:latest' remotely...image found: img_3mno4wk809624k18Watch your deployment at https://fly.io/apps/linkleaner/monitoring-------Updating existing machines in 'linkleaner' with rolling strategy------- ✔ [1/2] Cleared lease for 0801937b607358 ✔ [2/2] Cleared lease for 9080155dfe0de8-------\"><div></div></button></div></figure></div>"
    },
    {
      "id": "https://msfjarvis.dev/posts/assorted-git-things/",
      "url": "https://msfjarvis.dev/posts/assorted-git-things/",
      "title": "Assorted Git things",
      "date_published": "2024-10-01T08:27:00.000Z",
      "summary": "A running log of Git concepts I've learned since I started journaling",
      "content_html": "<p>When I asked for suggestions about note-taking apps <a href=\"https://masto-archive.msfjarvis.dev/@msfjarvis/112378523734491769\">back in May</a> I wasn’t fully convinced I’d be able to stick with it, but nearly 6 months later it has probably been the thing I’ve been most consistent about. Anyways, here’s a rather short list of Git things off the <a href=\"https://dictionary.cambridge.org/us/dictionary/english/til\">‘TIL’</a> list in my Logseq graph. I’ll keep adding new things to the top as they come up.</p>\n<h1 id=\"git_editor-variable\">GIT_EDITOR variable</h1>\n<p>While Git will usually pick the <code>$EDITOR</code> environment variable to present an edit UI for commit messages and rebase to-dos, it also supports <a href=\"https://git-scm.com/docs/git-var#Documentation/git-var.txt-GITEDITOR\">its own <code>$GIT_EDITOR</code></a> variable that can be used to set a Git-specific editor. This functionality can be leveraged from integrated terminals inside IDEs to allow writing commit message from the IDE without having to use their entire Git UI.</p>\n<h1 id=\"git-add\"><code>git add</code></h1>\n<ul>\n<li><code>git add -u</code> will stage changes to all tracked files and leave untracked ones alone.</li>\n<li><code>git add -N</code> will stage a file without any of its changes. You can treat that as an “intention” to create a file as well as making it easier to use <code>git diff</code> to see what is being added without needing the <code>--cached</code> flag every time.</li>\n</ul>\n<h1 id=\"git-submodules\">Git submodules</h1>\n<p>Probably the biggest pain in the ass in all of Git land. Every time I’ve had to use a submodule, I’ve disliked the experience. Here’s how to <em>properly</em> get rid of a submodule in case you’re switching to <a href=\"https://github.com/ingydotnet/git-subrepo\">git-subrepo</a> or a different solution.</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plain\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">git submodule deinit -f path/to/module</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">rm -rf .git/modules/path/to/module</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">git config -f .gitmodules --remove-section submodule.path/to/module</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">git add .gitmodules</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">git rm --cached path/to/module</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"git submodule deinit -f path/to/modulerm -rf .git/modules/path/to/modulegit config -f .gitmodules --remove-section submodule.path/to/modulegit add .gitmodulesgit rm --cached path/to/module\"><div></div></button></div></figure></div>\n<p>In order, this does the following:</p>\n<ol>\n<li><a href=\"https://git-scm.com/docs/git-submodule#Documentation/git-submodule.txt-deinit-f--force--all--ltpathgt82308203\"><code>deinit</code></a> removes the checkout of the submodule and removes it from <code>.git/config</code></li>\n<li>Deletes the bare repository Git creates of the submodule which is used to generate the actual checkout</li>\n<li>Edits your <code>.gitmodules</code> file to remove the submodule entry</li>\n<li>Stages the changes to <code>.gitmodules</code></li>\n<li>Deletes the submodule fully from the index</li>\n</ol>\n<p>At this point you can commit the changes and hopefully never deal with a submodule again :D</p>\n<h1 id=\"git-credential-helpers\">Git credential helpers</h1>\n<p>While trying to configure <a href=\"https://github.com/languitar/pass-git-helper\">pass-git-helper</a> to authenticate with my <a href=\"https://git.msfjarvis.dev/explore/repos\">personal Git forge</a> I ended up learning a bit about how Git interacts with credential helpers</p>\n<p>Git credential helpers work via writing and reading from stdin and stdout which helped me debug an issue I was having with pass-git-helper. To get a credential helper to spit out the password, it needs to be called with <code>get</code> as the first argument and pass in some basic key=value data. The interaction looks somewhat like given below, with <code>&lt;</code> indicating the stdin of the credential helper and <code>&gt;</code> indicating its stdout.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plain\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\"># for URL https://git.msfjarvis.dev/msfjarvis/super-private</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">$ pass-git-helper get</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">&lt; protocol=https</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">&lt; host=git.msfjarvis.dev</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">&lt; path=msfjarvis/super-private</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">&gt; protocol=https</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">&gt; host=git.msfjarvis.dev</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">&gt; username=msfjarvis</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">&gt; password=hunter2</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"# for URL https://git.msfjarvis.dev/msfjarvis/super-private$ pass-git-helper get< protocol=https< host=git.msfjarvis.dev< path=msfjarvis/super-private> protocol=https> host=git.msfjarvis.dev> username=msfjarvis> password=hunter2\"><div></div></button></div></figure></div>\n<h1 id=\"maintaining-a-fork-in-the-presence-of-fetchprunetags\">Maintaining a fork in the presence of <code>fetch.pruneTags</code></h1>\n<p>Enabling <code>fetch.pruneTags</code> causes my Nixpkgs Git checkout to constantly delete and fetch tags, since my fork is missing most tags that upstream has so they keep getting pruned when updating from <code>origin</code> and get re-created when fetching <code>upstream</code>.</p>\n<p>One solution suggested for this was setting <code>remotes.&lt;name&gt;.tagOpt = \"--no-tags\"</code> but that didn’t do the job for me.</p>\n<p>The thing that worked was to conditionally disable <code>pruneTags</code> for just the <code>origin</code> remote so it would not try to clear out the tags pulled from <code>upstream</code>. Achieved by running <code>git config --local remotes.origin.pruneTags false</code>.</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/a-tour-of-my-screenshots-folder/",
      "url": "https://msfjarvis.dev/posts/a-tour-of-my-screenshots-folder/",
      "title": "A tour of my screenshots folder",
      "date_published": "2024-04-01T06:30:00.000Z",
      "summary": "The history of my Minecraft adventures as told by my screenshots folder",
      "content_html": "<h2 id=\"preface\">Preface</h2>\n<blockquote>\n<p>This is a post for the <a href=\"https://aprilcools.club\">April Cools Club</a> which encourages people to break away from the typical cringiness of April Fools and do things you don’t normally do.</p>\n</blockquote>\n<p>To set the stage, every screenshot you will see going forward is gonna be Minecraft. I just love the game and I have had such fun with it for the past 5 years that it feels remiss to not share every so often (which I do these days at <a href=\"https://mstdn.games/@msfjarvis\">@msfjarvis@mstdn.games</a>). These are gonna be in order from oldest to newest, and I’ll try to annotate each screenshot with dates, alt text and the relevant anecdote as I remember them but honestly a bunch of this is just goofy shit I happened to capture.</p>\n<h2 id=\"just-a-bridge-really\">Just a bridge, really</h2>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/starter-base-bridge.D8R44tai.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/starter-base-bridge.D8R44tai.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" data-pswp-caption=\"Date taken: August 2, 2022\" aria-label=\"A cinematic shot of a tiny wooden bridge spanning two cliff sides on either side of the frame. The bridge has evenly spaced poles on either side of it with a shroomlight to illuminate the entire thing. The scene is set in the night time and the Aurora Borealis is visible in the background.\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/starter-base-bridge.D8R44tai.webp\" alt=\"A cinematic shot of a tiny wooden bridge spanning two cliff sides on either side of the frame. The bridge has evenly spaced poles on either side of it with a shroomlight to illuminate the entire thing. The scene is set in the night time and the Aurora Borealis is visible in the background.\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Date taken: August 2, 2022</figcaption></figure>\n<p>This bridge is in a Minecraft world that <a href=\"https://sasikanth.dev/me/\">Sasikanth</a> and I started in the second half of 2022, and built entirely by him next to our starter base. After Sasi kinda moved on from playing on the server (as Minecraft players inevitably do, myself included) I copied the world and started using it as my singleplayer world and I still play on it to this day.</p>\n<h2 id=\"an-unlikely-friendship\">An unlikely friendship</h2>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/an-unlikely-friendship.LLoxwnG3.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/an-unlikely-friendship.LLoxwnG3.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" data-pswp-caption=\"Date taken: August 13, 2022\" aria-label=\"A blacksmith villager and a creeper standing right next to each other in the night. The villager is facing the creeper while it looks off into the distance, towards the right side of the camera.\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/an-unlikely-friendship.LLoxwnG3.webp\" alt=\"A blacksmith villager and a creeper standing right next to each other in the night. The villager is facing the creeper while it looks off into the distance, towards the right side of the camera.\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Date taken: August 13, 2022</figcaption></figure>\n<p>I don’t think I really remember where this is from, but if I had to guess it was the village I and Sasikanth discovered and promptly lay ruin to which today happens to be my full time base.</p>\n<h2 id=\"the-start-of-the-storage-room\">The start of the storage room</h2>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/the-start-of-the-storage-room.DZChJ5wk.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/the-start-of-the-storage-room.DZChJ5wk.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" data-pswp-caption=\"Date taken: August 20, 2022\" aria-label=\"An incomplete rectangular arrangement of double chests with item frames on them, with a mess of shulker boxes in between and me standing on top of them\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/the-start-of-the-storage-room.DZChJ5wk.webp\" alt=\"An incomplete rectangular arrangement of double chests with item frames on them, with a mess of shulker boxes in between and me standing on top of them\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Date taken: August 20, 2022</figcaption></figure>\n<p>After commandeering the aforementioned village I decided to lay roots next door, and this is basically the start of my storage room. The basic design is still the same, but it has like, walls and stuff now.</p>\n<h2 id=\"i-am-a-dwarf-and-im-digging-a-hole\">I am a dwarf, and I’m digging a hole</h2>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/diggy-diggy-hole.N1WZgKVO.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/diggy-diggy-hole.N1WZgKVO.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" data-pswp-caption=\"Date taken: December 29, 2022\" aria-label=\"A top down shot of my Minecraft character standing next to a one chunk big hole straight down to bedrock\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/diggy-diggy-hole.N1WZgKVO.webp\" alt=\"A top down shot of my Minecraft character standing next to a one chunk big hole straight down to bedrock\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Date taken: December 29, 2022</figcaption></figure>\n<p>Honestly not much to say, I was in a bit of a slump with my mental health and decided the best use of my mushy brain was to dig down a whole chunk to eventually build a slime farm.</p>\n<h2 id=\"did-i-drain-this-ocean-monument-or-did-it-drain-me\">Did I drain this Ocean Monument or did it drain me?</h2>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/draining-ocean-monument-part-1.CD0fIRyt.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/draining-ocean-monument-part-1.CD0fIRyt.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" data-pswp-caption=\"Date taken: February 27, 2023\" aria-label=\"Cinematic night time shot of my character standing on a wall of sand on the close left side of the screen while an Ocean Monument takes up the rest of the bottom half\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/draining-ocean-monument-part-1.CD0fIRyt.webp\" alt=\"Cinematic night time shot of my character standing on a wall of sand on the close left side of the screen while an Ocean Monument takes up the rest of the bottom half\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Date taken: February 27, 2023</figcaption></figure>\n<p>I didn’t play much for a month or two, so I decided to pick up a somewhat involved project to get me back in the swing of things, which happened to be a Guardian farm for the prismarine family of blocks. I’ll let the other screenshots paint the picture, but suffice to say I had under-estimated the scope of this 😬</p>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/draining-ocean-monument-part-2.BSxw5H7H.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/draining-ocean-monument-part-2.BSxw5H7H.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" data-pswp-caption=\"Date taken: March 7, 2023\" aria-label=\"Overhead world map shot of the Ocean monument with approximately 30% of it drained. There is a perimeter of sand around it as well as some evenly spaced walls running across the screen to section off slices to be drained.\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/draining-ocean-monument-part-2.BSxw5H7H.webp\" alt=\"Overhead world map shot of the Ocean monument with approximately 30% of it drained. There is a perimeter of sand around it as well as some evenly spaced walls running across the screen to section off slices to be drained.\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Date taken: March 7, 2023</figcaption></figure>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/draining-ocean-monument-part-3.D4Mrzeq5.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/draining-ocean-monument-part-3.D4Mrzeq5.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" data-pswp-caption=\"Date taken: March 12, 2023\" aria-label=\"The same setup described before but with about 55% of the structure drained.\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/draining-ocean-monument-part-3.D4Mrzeq5.webp\" alt=\"The same setup described before but with about 55% of the structure drained.\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Date taken: March 12, 2023</figcaption></figure>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/draining-ocean-monument-part-4.BBLU8D2n.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/draining-ocean-monument-part-4.BBLU8D2n.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" data-pswp-caption=\"Date taken: March 15, 2023\" aria-label=\"The entire structure is now drained, with just the sand perimeter remaining around it\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/draining-ocean-monument-part-4.BBLU8D2n.webp\" alt=\"The entire structure is now drained, with just the sand perimeter remaining around it\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Date taken: March 15, 2023</figcaption></figure>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/draining-ocean-monument-part-5.DontZOAn.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/draining-ocean-monument-part-5.DontZOAn.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" data-pswp-caption=\"Date taken: March 20, 2023\" aria-label=\"Me standing on top of one of the sand walls, looking inwards to the now completed Guardian farm. It's comprimised of two glass tanks full of water that funnel guardians into a central chamber where they fall and have their drops collected underground\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/draining-ocean-monument-part-5.DontZOAn.webp\" alt=\"Me standing on top of one of the sand walls, looking inwards to the now completed Guardian farm. It's comprimised of two glass tanks full of water that funnel guardians into a central chamber where they fall and have their drops collected underground\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Date taken: March 20, 2023</figcaption></figure>\n<p>When this was finally done I genuinely used it like 4 times total, turns out I’m not really a prismarine guy so that’s a couple of weeks I am not getting back.</p>\n<h2 id=\"getting-real-personal-with-a-warden\">Getting real personal with a Warden</h2>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/meeting-a-warden.BaGN6Hjk.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/meeting-a-warden.BaGN6Hjk.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" data-pswp-caption=\"Date taken: March 12, 2023\" aria-label=\"A very close over the shoulder shot of me mere inches from a Warden which is staring right into my soul\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/meeting-a-warden.BaGN6Hjk.webp\" alt=\"A very close over the shoulder shot of me mere inches from a Warden which is staring right into my soul\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Date taken: March 12, 2023</figcaption></figure>\n<p>To break up the monotony of placing sand for the Guardian farm I paid a visit to a nearby Ancient City and ended up a little too close to a Warden, which did eventually kill me.</p>\n<h2 id=\"the-sea-shanty-era\">The sea shanty era</h2>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/tragic-lovers.bGCHaiki.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/tragic-lovers.bGCHaiki.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" data-pswp-caption=\"Date taken: June 27, 2023\" aria-label=\"Two players in a bamboo raft that is positioned on the bow of a shipwreck which is poking out of water\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/tragic-lovers.bGCHaiki.webp\" alt=\"Two players in a bamboo raft that is positioned on the bow of a shipwreck which is poking out of water\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Date taken: June 27, 2023</figcaption></figure>\n<p>I took another couple months off and then set up a small server with a handful of friends to mess around on a fresh world with relative newbies to the game. A lot of chaos ensued, but a large chunk of the time I spent on the server ended up being me and <a href=\"https://yashgarg.dev/\">Yash</a> just boating across oceans looking for anything mildly interesting while in a voice call with our friends. It was fun times, but as always people’s interest dwindled down and I shut the server down a couple of weeks later.</p>\n<h2 id=\"excavating-the-nether\">Excavating the Nether</h2>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/nether-mining-pre-boom.KcHWTiCz.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/nether-mining-pre-boom.KcHWTiCz.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" data-pswp-caption=\"Date taken: July 11, 2023\" aria-label=\"A map of the Nether at elevation Y=0, showing parallel one block wide tunnels going across chunk borders and filled with unevenly spaced blocks of TNT\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/nether-mining-pre-boom.KcHWTiCz.webp\" alt=\"A map of the Nether at elevation Y=0, showing parallel one block wide tunnels going across chunk borders and filled with unevenly spaced blocks of TNT\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Date taken: July 11, 2023</figcaption></figure>\n<p>I wanted Ancient Debris for a project and decided to just get a whole load of it at once, which resulted in this set of TNT-filled tunnels. Here’s the damage all the TNT did:</p>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/nether-mining-post-boom.BBgUP1zB.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/nether-mining-post-boom.BBgUP1zB.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" data-pswp-caption=\"Date taken: July 11, 2023\" aria-label=\"The same tunnels from above after all the TNT in them was lit. They are now wider, more jagged and a lot more lava-filled\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/nether-mining-post-boom.BBgUP1zB.webp\" alt=\"The same tunnels from above after all the TNT in them was lit. They are now wider, more jagged and a lot more lava-filled\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Date taken: July 11, 2023</figcaption></figure>\n<h2 id=\"my-first-sniffers\">My first Sniffers</h2>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/first-sniffers.Bngl6_9J.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/first-sniffers.Bngl6_9J.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" data-pswp-caption=\"Date taken: July 12, 2023\" aria-label=\"A fenced off patch of moss with me standing in the middle and one Sniffer egg on each side\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/first-sniffers.Bngl6_9J.webp\" alt=\"A fenced off patch of moss with me standing in the middle and one Sniffer egg on each side\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Date taken: July 12, 2023</figcaption></figure>\n<p>When the Sniffers were added to the game I just had to get them, and obviously then I created a farm for the seeds they “sniff” up.</p>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/sniffer-for-a-sniffer-farm.DzCVNMyU.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/sniffer-for-a-sniffer-farm.DzCVNMyU.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" data-pswp-caption=\"Date taken: September 13, 2023\" aria-label=\"Elevated shot of a large Sniffer build, the back of it is built with colored glass and you can see some 10 Sniffers inside on a floor of mud blocks\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/sniffer-for-a-sniffer-farm.DzCVNMyU.webp\" alt=\"Elevated shot of a large Sniffer build, the back of it is built with colored glass and you can see some 10 Sniffers inside on a floor of mud blocks\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Date taken: September 13, 2023</figcaption></figure>\n<h2 id=\"the-end-ring-project\">The End Ring Project</h2>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/incomplete-end-island-ring.e2rNVVEa.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/incomplete-end-island-ring.e2rNVVEa.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" data-pswp-caption=\"Date taken: March 28, 2024\" aria-label=\"Top down shot of the main End island, showing a Prismarine ring going through all the end gateways which are illuminated by Prismarine Lights\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/incomplete-end-island-ring.e2rNVVEa.webp\" alt=\"Top down shot of the main End island, showing a Prismarine ring going through all the end gateways which are illuminated by Prismarine Lights\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Date taken: March 28, 2024</figcaption></figure>\n<p>I envisioned a continuous ring of Prismarine walkways around all the End gateways to be a cool way to get to them rather than just pillaring up, but I biffed the “circle” so many times that it’s kinda stalled at the moment.</p>\n<h2 id=\"all-trimmed-up\">All Trimmed Up</h2>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/obtaining-every-armor-trim.Ckn5lEi4.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/obtaining-every-armor-trim.Ckn5lEi4.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" data-pswp-caption=\"Date taken: March 16, 2024\" aria-label=\"A short hallway of Spruce planks showcasing every single Armor Trim as of Minecraft 1.20.4\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/obtaining-every-armor-trim.Ckn5lEi4.webp\" alt=\"A short hallway of Spruce planks showcasing every single Armor Trim as of Minecraft 1.20.4\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Date taken: March 16, 2024</figcaption></figure>\n<p>I went on a quest to obtain every single armor trim and enough Netherite to create armor to put them on, which in total probably took 20-odd hours including all the Diamond and Ancient Debris mining as well as actually locating all the trims. My Minecraft closet has more variety than my IRL one which feels like cause for concern.</p>\n<h2 id=\"bonus-randomness\">Bonus randomness</h2>\n<h3 id=\"the-minecraft-x-faze-collab\">The Minecraft x FaZe collab</h3>\n<p>For some reason Minecraft likes to spawn Nether Fossils that look way too much like the [FaZe Clan] logo and I apparently have a bunch of them screenshotted, so here they are:</p>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/fazeup-1.qSfI8eKZ.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/fazeup-1.qSfI8eKZ.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" aria-label=\"The FaZe Fossil in a soul sand valley\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/fazeup-1.qSfI8eKZ.webp\" alt=\"The FaZe Fossil in a soul sand valley\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a></figure>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/fazeup-2.kWjX4XFR.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/fazeup-2.kWjX4XFR.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" aria-label=\"Another FaZe Fossil in a soul sand valley\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/fazeup-2.kWjX4XFR.webp\" alt=\"Another FaZe Fossil in a soul sand valley\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a></figure>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/fazeup-3.Berus8n1.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/fazeup-3.Berus8n1.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" aria-label=\"Yet Another FaZe Fossil in a soul sand valley\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/fazeup-3.Berus8n1.webp\" alt=\"Yet Another FaZe Fossil in a soul sand valley\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a></figure>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/fazeup-4.CuW6t_kI.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/fazeup-4.CuW6t_kI.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" aria-label=\"Would you believe it? Another FaZe logo in a soul sand valley\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/fazeup-4.CuW6t_kI.webp\" alt=\"Would you believe it? Another FaZe logo in a soul sand valley\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a></figure>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/fazeup-5.BeulB0Mk.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/fazeup-5.BeulB0Mk.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" aria-label=\"To break up the monotony, this FaZe logo was in a 'Quartz Flats' custom biome from the Incendium datapack\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/fazeup-5.BeulB0Mk.webp\" alt=\"To break up the monotony, this FaZe logo was in a 'Quartz Flats' custom biome from the Incendium datapack\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a></figure>\n<h3 id=\"the-impossible-portal\">The impossible portal</h3>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/chunk-pruned-portal.CzSAfSbM.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/chunk-pruned-portal.CzSAfSbM.webp\" data-pswp-width=\"1920\" data-pswp-height=\"1080\" data-pswp-caption=\"Date taken: June 12, 2023\" aria-label=\"A Nether portal with its left side of Obsidian blocks missing but the portal is still intact\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/chunk-pruned-portal.CzSAfSbM.webp\" alt=\"A Nether portal with its left side of Obsidian blocks missing but the portal is still intact\" loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Date taken: June 12, 2023</figcaption></figure>\n<p>I manually prune unused chunks before Minecraft updates in order to let them regenerate with the updated terrain and accidentally sliced off this portal as it happened to be on a chunk boundary. It’s all the way back at my starter base so it does not see much use, but it’s there.</p>\n<h2 id=\"the-end\">The end…?</h2>\n<p>I don’t know why I picked this to do of all the things I could have for April Cools. However, reliving all the memories of fun times I had with my friends and even by myself were worth the pain it was to go through 800 odd screenshots and find moderately interesting things :)</p>\n<p>Hopefully I have something cooler for next year and that I actually give myself more than 4 hours to write it up.</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/improving-dependency-sync-speeds-for-your-gradle-project/",
      "url": "https://msfjarvis.dev/posts/improving-dependency-sync-speeds-for-your-gradle-project/",
      "title": "Improving dependency sync speeds for your Gradle project",
      "date_published": "2024-03-30T21:43:07.000Z",
      "summary": "Waiting for Gradle to download dependencies is so 2023",
      "content_html": "<p>Android developers are intimately familiar with the ritual of staring at your IDE for tens of minutes while Gradle imports a new project before they can start working on it. While not fully avoidable, there are many ways to improve the situation. For small to medium projects, the time spent on this import phase can be largely dominated by dependency downloads.</p>\n<h2 id=\"preface\">Preface</h2>\n<p>This post is going to assume some things about you and your project, but you should be fine even if these aren’t true for you.</p>\n<ul>\n<li>You’re somewhat comfortable mucking around with Gradle</li>\n<li>Your project is using Gradle 8.7, the latest as of writing</li>\n</ul>\n<p>If you’re stuck on a lower version of Gradle, you will hit <a href=\"https://github.com/gradle/gradle/issues/26569\">this bug</a> with the code samples in the post. Replacing all calls to <code>includeGroupAndSubgroups</code> with <code>includeGroupByRegex</code> can let you work around it temporarily (Note the addition of the <code>.*</code> at the end):</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line highlight del\"><div class=\"code\"><span style=\"--0:#484b64;--1:#EBDBB2\">includeGroupAndSubgroups(\"com.example\")</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66;--1:#EBDBB2\">includeGroupByRegex(\"com.example.*\")</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"includeGroupAndSubgroups(&quot;com.example&quot;)includeGroupByRegex(&quot;com.example.*&quot;)\"><div></div></button></div></figure></div>\n<h2 id=\"obtaining-a-baseline\">Obtaining a baseline</h2>\n<p>To get an idea for how long it actually takes your project to fetch its dependencies and to establish a baseline to compare improvements again, we can leverage Android Studio’s relatively new <a href=\"https://developer.android.com/studio/releases/past-releases/as-giraffe-release-notes#download-info-sync\">Downloads Info</a> view to see how many network requests are being made and how many of those are failing and contributing to our slower build. Gradle has a <code>--refresh-dependencies</code> flag which ignores its existing cache of downloaded dependencies and redownloads them from the remote repositories which will allow us to get consistent results, barring network and disk fluctuations.</p>\n<p>In Android Studio, create a new run configuration for Gradle’s in-built <code>dependencies</code> task that will resolve all configurations and give us a more representative number. The <code>--refresh-dependencies</code> flag will force a full re-download to ensure caches do not affect our benchmarks:</p>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/run-configuration.CXwhjZor.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/run-configuration.CXwhjZor.webp\" data-pswp-width=\"1024\" data-pswp-height=\"735\" data-pswp-caption=\"The Android Studio Run configuration window configured with the task ':android:dependencies --refresh-dependencies'\" aria-label=\"The Android Studio Run configuration window configured with the task ':android:dependencies --refresh-dependencies'\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/run-configuration.CXwhjZor.webp\" alt=\"\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"735\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">The Android Studio Run configuration window configured with the task ':android:dependencies --refresh-dependencies'</figcaption></figure>\n<p>When you run this task, you’ll see the Build tool window at the bottom get populated with logs of Gradle downloading dependencies and the Downloads info tab will start accumulating the statistics for it.</p>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/download-info.CKvaU4CC.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/download-info.CKvaU4CC.webp\" data-pswp-width=\"1220\" data-pswp-height=\"504\" data-pswp-caption=\"Download info window showing a list of network requests and their sources while the task is running\" aria-label=\"Download info window showing a list of network requests and their sources while the task is running\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/download-info.CKvaU4CC.webp\" alt=\"\" loading=\"lazy\" decoding=\"async\" width=\"1220\" height=\"504\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">Download info window showing a list of network requests and their sources while the task is running</figcaption></figure>\n<h2 id=\"how-gradle-fetches-your-dependencies\">How Gradle fetches your dependencies</h2>\n<p>The Gradle documentation for <a href=\"https://docs.gradle.org/current/userguide/dependency_resolution.html#sec:how-gradle-downloads-deps\">dependency resolution</a> explains in some depth how the version conflict resolution and caching systems work, but that’s pretty jargon-heavy and too much of a detour from what we’re really here to do so I’ll just Spark Notes™️ it and move on to more fun stuff.</p>\n<ul>\n<li>Gradle requires declaring <strong>repositories</strong> where dependencies are fetched from.</li>\n<li>Dependencies are looked up in each repository, <strong>in declaration order</strong>, until they are found in one.</li>\n<li>Gradle makes a lot of network requests as part of this lookup, and it is in our interest to reduce them.</li>\n</ul>\n<h2 id=\"a-quick-attempt-at-optimisation\">A quick attempt at optimisation</h2>\n<p>In the previous section I started off by mentioning <strong>repositories</strong>, which define <em>where</em> Gradle will look for dependencies. The repositories setup for a typical Android project might look something like this:</p>\n<div class=\"expressive-code\"><figure class=\"frame has-title\"><figcaption class=\"header\"><span class=\"title\">settings.gradle.kts</span></figcaption><pre data-language=\"kotlin\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">pluginManagement</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">repositories</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">mavenCentral</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">google</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">dependencyResolutionManagement</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">repositories</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">maven</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"https://jitpack.io\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) { name </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"JitPack\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">google</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">mavenCentral</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">maven</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"https://oss.sonatype.org/content/repositories/snapshots/\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">name </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"Sonatype snapshots</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">    </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">  </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"pluginManagement {  repositories {    mavenCentral()    google()  }}dependencyResolutionManagement {  repositories {    maven(&quot;https://jitpack.io&quot;) { name = &quot;JitPack&quot; }    google()    mavenCentral()    maven(&quot;https://oss.sonatype.org/content/repositories/snapshots/&quot;) {      name = &quot;Sonatype snapshots    }  }}\"><div></div></button></div></figure></div>\n<p>This tells Gradle that you want plugin dependencies to be looked up from <a href=\"https://repo1.maven.org/maven2/\">Maven Central</a> and <a href=\"https://maven.google.com/web/index.html\">gMaven</a>, and for all other dependencies to come from <a href=\"https://jitpack.io\">JitPack</a>, <a href=\"https://maven.google.com/web/index.html\">gMaven</a>, <a href=\"https://repo1.maven.org/maven2/\">Maven Central</a>, or the <a href=\"https://oss.sonatype.org/content/repositories/snapshots/com/squareup/sqldelight/\">Maven Central snapshots</a> repository; <strong>in that order</strong>. The first simple tweak you can make is to reorder these based on how many of your dependencies you expect to come from what repository.</p>\n<p>For example, in a typical Android build most of your plugin classpath will be dominated by the Android Gradle Plugin (AGP), so you’d want gMaven to be come first so that Gradle does not waste time trying to find AGP on Maven Central.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">pluginManagement {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">repositories {</span></div></div><div class=\"ec-line highlight del\"><div class=\"code\"><span style=\"--0:#484b64\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">mavenCentral()</span></span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">google()</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">mavenCentral()</span></span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"pluginManagement {  repositories {    mavenCentral()    google()    mavenCentral()  }}\"><div></div></button></div></figure></div>\n<p>For the <code>dependencyResolutionManagement</code> block, JitPack is very likely to only host one or two of the dependencies you require, while most of them will be coming from gMaven and Maven Central, so shifting JitPack to the very end will significantly reduce the number of failed requests.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">dependencyResolutionManagement {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">repositories {</span></div></div><div class=\"ec-line highlight del\"><div class=\"code\"><span style=\"--0:#484b64\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">maven(\"https://jitpack.io\") { name = \"JitPack\" }</span></span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">google()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">mavenCentral()</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">maven(\"https://jitpack.io\") { name = \"JitPack\" }</span></span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">maven(\"https://oss.sonatype.org/content/repositories/snapshots/\") {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">name = \"Sonatype snapshots</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"dependencyResolutionManagement {  repositories {    maven(&quot;https://jitpack.io&quot;) { name = &quot;JitPack&quot; }    google()    mavenCentral()    maven(&quot;https://jitpack.io&quot;) { name = &quot;JitPack&quot; }    maven(&quot;https://oss.sonatype.org/content/repositories/snapshots/&quot;) {      name = &quot;Sonatype snapshots    }  }}\"><div></div></button></div></figure></div>\n<h2 id=\"going-for-zero\">Going for zero</h2>\n<p>With the minor changes made above we have already significantly improved on our failed requests metric, but why stop at good when we can have <em>perfect</em>.</p>\n<p>Gradle’s repositories APIs also support the notion of specifying the expected “contents” of individual repositories, which tells Gradle what groups of dependencies are supposed to be available in what repositories. This allows it to prevent redundant network requests and significantly boosts sync performance.</p>\n<p>These filters can be of two types:</p>\n<ul>\n<li>Declaring that certain artifacts can <em>only</em> be resolved from certain repositories: <a href=\"https://docs.gradle.org/current/kotlin-dsl/gradle/org.gradle.api.artifacts.dsl/-repository-handler/exclusive-content.html?query=abstract%20fun%20exclusiveContent(action:%20Action%3Cout%20Any%3E)\"><code>exclusiveContent</code></a></li>\n<li>Declaring that a repository <em>only</em> contains certain artifacts: <a href=\"https://docs.gradle.org/current/kotlin-dsl/gradle/org.gradle.api.artifacts.repositories/-artifact-repository/content.html?query=abstract%20fun%20content(configureAction:%20Action%3Cout%20Any%3E)\"><code>content</code></a></li>\n</ul>\n<p>The difference is subtle, but should become clearer shortly as we start hacking on our setup.</p>\n<p>For our plugins block, we want only gMaven to supply AGP and everything else can come from Maven Central. Here’s how to achieve that:</p>\n<div class=\"expressive-code\"><figure class=\"frame has-title\"><figcaption class=\"header\"><span class=\"title\">settings.gradle.kts</span></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">pluginManagement {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">repositories {</span></div></div><div class=\"ec-line highlight del\"><div class=\"code\"><span style=\"--0:#484b64\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">google()</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">exclusiveContent { // First type of filter</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">      </span></span><span style=\"--1:#EBDBB2\">forRepository { google() } // Specify the repository this applies to</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">      </span></span><span style=\"--1:#EBDBB2\">filter { // Start specifying what dependencies are *only* found in this repo</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">        </span></span><span style=\"--1:#EBDBB2\">includeGroupAndSubgroups(\"androidx\")</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">        </span></span><span style=\"--1:#EBDBB2\">includeGroupAndSubgroups(\"com.android\")</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">        </span></span><span style=\"--1:#EBDBB2\">includeGroup(\"com.google.testing.platform\")</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">      </span></span><span style=\"--1:#EBDBB2\">}</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">}</span></span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">mavenCentral()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"pluginManagement {  repositories {    google()    exclusiveContent { // First type of filter      forRepository { google() } // Specify the repository this applies to      filter { // Start specifying what dependencies are *only* found in this repo        includeGroupAndSubgroups(&quot;androidx&quot;)        includeGroupAndSubgroups(&quot;com.android&quot;)        includeGroup(&quot;com.google.testing.platform&quot;)      }    }    mavenCentral()  }}\"><div></div></button></div></figure></div>\n<p>For other dependencies that are governed by the <code>dependencyResolutionManagement</code> block, the setup is similar. To demonstrate the usage of the second kind of filter, we’re introducing an additional constraint: assume the build relies on the <a href=\"https://developer.android.com/jetpack/androidx/releases/compose-compiler\">Jetpack Compose Compiler</a>, and we go back and forth between stable and pre-release builds of it. The pre-release builds can only be obtained from <a href=\"https://androidx.dev/storage/compose-compiler/repository\">androidx.dev</a>, while the stable builds only exist on <a href=\"https://maven.google.com/web/index.html\">gMaven</a>. If we tried to use <code>exclusiveContent</code> here, it would make Gradle only check one of the declared repositories for the artifact and fail if it doesn’t find it there. To allow this fallback, we instead use a <code>content</code> filter as follows.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">dependencyResolutionManagement {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">repositories {</span></div></div><div class=\"ec-line highlight del\"><div class=\"code\"><span style=\"--0:#484b64\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">google()</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">google {</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">      </span></span><span style=\"--1:#EBDBB2\">content {</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">        </span></span><span style=\"--1:#EBDBB2\">includeGroupAndSubgroups(\"androidx\")</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">        </span></span><span style=\"--1:#EBDBB2\">includeGroupAndSubgroups(\"com.android\")</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">        </span></span><span style=\"--1:#EBDBB2\">includeGroupAndSubgroups(\"com.google\")</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">      </span></span><span style=\"--1:#EBDBB2\">}</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">}</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">maven(\"https://androidx.dev/storage/compose-compiler/repository\") {</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">      </span></span><span style=\"--1:#EBDBB2\">name = \"Compose Compiler Snapshots\"</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">      </span></span><span style=\"--1:#EBDBB2\">content { includeGroup(\"androidx.compose.compiler\") }</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">}</span></span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">mavenCentral()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">maven(\"https://jitpack.io\") { name = \"JitPack\" }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">maven(\"https://oss.sonatype.org/content/repositories/snapshots/\") {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">name = \"Sonatype snapshots</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"dependencyResolutionManagement {  repositories {    google()    google {      content {        includeGroupAndSubgroups(&quot;androidx&quot;)        includeGroupAndSubgroups(&quot;com.android&quot;)        includeGroupAndSubgroups(&quot;com.google&quot;)      }    }    maven(&quot;https://androidx.dev/storage/compose-compiler/repository&quot;) {      name = &quot;Compose Compiler Snapshots&quot;      content { includeGroup(&quot;androidx.compose.compiler&quot;) }    }    mavenCentral()    maven(&quot;https://jitpack.io&quot;) { name = &quot;JitPack&quot; }    maven(&quot;https://oss.sonatype.org/content/repositories/snapshots/&quot;) {      name = &quot;Sonatype snapshots    }  }}\"><div></div></button></div></figure></div>\n<p>This setup tells Gradle the specific artifacts present in these repositories but does not enforce any restrictions on which repository said artifacts can come from. Now, if I use a pre-release version of the Compose Compiler, Gradle will first try to look it up in <a href=\"https://maven.google.com/web/index.html\">gMaven</a> and then fall back to the <code>androidx.dev</code> repository.</p>\n<p>In the above example we also see <a href=\"https://jitpack.io\">JitPack</a> being mentioned, which we only wish to use for a specific dependency that’s unavailable elsewhere. The <a href=\"https://docs.gradle.org/current/kotlin-dsl/gradle/org.gradle.api.artifacts.dsl/-repository-handler/exclusive-content.html?query=abstract%20fun%20exclusiveContent(action:%20Action%3Cout%20Any%3E)\"><code>exclusiveContent</code></a> filter is precisely for this use case:</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">dependencyResolutionManagement {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">repositories {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">google {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">content {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">        </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">includeGroupAndSubgroups(\"androidx\")</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">        </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">includeGroupAndSubgroups(\"com.android\")</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">        </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">includeGroupAndSubgroups(\"com.google\")</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">maven(\"https://androidx.dev/storage/compose-compiler/repository\") {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">name = \"Compose Compiler Snapshots\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">content { includeGroup(\"androidx.compose.compiler\") }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">mavenCentral()</span></div></div><div class=\"ec-line highlight del\"><div class=\"code\"><span style=\"--0:#484b64\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">maven(\"https://jitpack.io\") { name = \"JitPack\" }</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">exclusiveContent {</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">      </span></span><span style=\"--1:#EBDBB2\">forRepository { maven(\"https://jitpack.io\") { name = \"JitPack\" } }</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">      </span></span><span style=\"--1:#EBDBB2\">filter { includeGroup(\"com.github.requery\") }</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">}</span></span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">maven(\"https://oss.sonatype.org/content/repositories/snapshots/\")</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"dependencyResolutionManagement {  repositories {    google {      content {        includeGroupAndSubgroups(&quot;androidx&quot;)        includeGroupAndSubgroups(&quot;com.android&quot;)        includeGroupAndSubgroups(&quot;com.google&quot;)      }    }    maven(&quot;https://androidx.dev/storage/compose-compiler/repository&quot;) {      name = &quot;Compose Compiler Snapshots&quot;      content { includeGroup(&quot;androidx.compose.compiler&quot;) }    }    mavenCentral()    maven(&quot;https://jitpack.io&quot;) { name = &quot;JitPack&quot; }    exclusiveContent {      forRepository { maven(&quot;https://jitpack.io&quot;) { name = &quot;JitPack&quot; } }      filter { includeGroup(&quot;com.github.requery&quot;) }    }    maven(&quot;https://oss.sonatype.org/content/repositories/snapshots/&quot;)  }}\"><div></div></button></div></figure></div>\n<p>The Sonatype OSS snapshots repository is only intended to be used for snapshot releases of dependencies we’d otherwise source from Maven Central, so we can indicate to Gradle to only search for snapshots in there with a <a href=\"https://docs.gradle.org/current/kotlin-dsl/gradle/org.gradle.api.artifacts.repositories/-maven-artifact-repository/maven-content.html?query=abstract%20fun%20mavenContent(configureAction:%20Action%3Cout%20Any%3E)\"><code>mavenContent</code></a> directive:</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">dependencyResolutionManagement {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">repositories {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">google {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">content {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">        </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">includeGroupAndSubgroups(\"androidx\")</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">        </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">includeGroupAndSubgroups(\"com.android\")</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">        </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">includeGroupAndSubgroups(\"com.google\")</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">maven(\"https://androidx.dev/storage/compose-compiler/repository\") {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">name = \"Compose Compiler Snapshots\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">content { includeGroup(\"androidx.compose.compiler\") }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">mavenCentral()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">maven(\"https://jitpack.io\")</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">exclusiveContent {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">forRepository { maven(\"https://jitpack.io\") { name = \"JitPack\" } }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">filter { includeGroup(\"com.github.requery\") }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">maven(\"https://oss.sonatype.org/content/repositories/snapshots/\") {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">name = \"Sonatype Snapshots\"</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">     </span></span><span style=\"--1:#EBDBB2\">mavenContent {</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">       </span></span><span style=\"--1:#EBDBB2\">snapshotsOnly()</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">     </span></span><span style=\"--1:#EBDBB2\">}</span></span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"dependencyResolutionManagement {  repositories {    google {      content {        includeGroupAndSubgroups(&quot;androidx&quot;)        includeGroupAndSubgroups(&quot;com.android&quot;)        includeGroupAndSubgroups(&quot;com.google&quot;)      }    }    maven(&quot;https://androidx.dev/storage/compose-compiler/repository&quot;) {      name = &quot;Compose Compiler Snapshots&quot;      content { includeGroup(&quot;androidx.compose.compiler&quot;) }    }    mavenCentral()    maven(&quot;https://jitpack.io&quot;)    exclusiveContent {      forRepository { maven(&quot;https://jitpack.io&quot;) { name = &quot;JitPack&quot; } }      filter { includeGroup(&quot;com.github.requery&quot;) }    }    maven(&quot;https://oss.sonatype.org/content/repositories/snapshots/&quot;) {      name = &quot;Sonatype Snapshots&quot;     mavenContent {       snapshotsOnly()     }    }  }}\"><div></div></button></div></figure></div>\n<h3 id=\"bonus-section-kotlin-multiplatform\">Bonus section: Kotlin Multiplatform</h3>\n<p>If you’re working with Kotlin Multiplatform, these directions will sadly not cover all the dependencies being fetched during your build. There’s a YouTrack issue (<a href=\"https://youtrack.jetbrains.com/issue/KT-51379\">KT-51379</a>) that you can subscribe to for updates on this, but in the mean time here’s the missing bits:</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"kotlin\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">dependencyResolutionManagement</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">repositories</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// workaround for https://youtrack.jetbrains.com/issue/KT-51379</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">exclusiveContent</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">forRepository</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">ivy</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"https://download.jetbrains.com/kotlin/native/builds\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">          </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">name </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"Kotlin Native\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">          </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">patternLayout</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">            </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">listOf</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">                </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"macos-x86_64\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">                </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"macos-aarch64\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">                </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"osx-x86_64\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">                </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"osx-aarch64\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">                </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"linux-x86_64\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">                </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"windows-x86_64\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">              </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">              </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">forEach</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> { os </span><span style=\"--0:#7B34D9;--1:#FE8929\">-&gt;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">                </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">listOf</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"dev\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"releases\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">).</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">forEach</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> { stage </span><span style=\"--0:#7B34D9;--1:#FE8929\">-&gt;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">                  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">artifact</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"</span><span style=\"--0:#4C4F69;--1:#91AFA3\">$stage</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">/[revision]/</span><span style=\"--0:#4C4F69;--1:#91AFA3\">$os</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">/[artifact]-[revision].[ext]\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">                </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">              </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">          </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">          </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">metadataSources</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> { </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">artifact</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">() }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">        </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">filter</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> { </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">includeModuleByRegex</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\".*\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\".*kotlin-native-prebuilt.*\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">exclusiveContent</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">forRepository</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">ivy</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"https://nodejs.org/dist/\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">          </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">name </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"Node Distributions at </span><span style=\"--0:#4C4F69;--1:#91AFA3\">$url</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">          </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">patternLayout</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> { </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">artifact</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"v[revision]/[artifact](-v[revision]-[classifier]).[ext]\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">          </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">metadataSources</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> { </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">artifact</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">() }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">          </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">content</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> { </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">includeModule</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"org.nodejs\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"node\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">        </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">filter</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> { </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">includeGroup</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"org.nodejs\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">exclusiveContent</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">forRepository</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">ivy</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"https://github.com/yarnpkg/yarn/releases/download\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">          </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">name </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"Yarn Distributions at </span><span style=\"--0:#4C4F69;--1:#91AFA3\">$url</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">          </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">patternLayout</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> { </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">artifact</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"v[revision]/[artifact](-v[revision]).[ext]\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">          </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">metadataSources</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> { </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">artifact</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">() }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">          </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">content</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> { </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">includeModule</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"com.yarnpkg\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"yarn\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">        </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">filter</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> { </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">includeGroup</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"com.yarnpkg\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"dependencyResolutionManagement {  repositories {    // workaround for https://youtrack.jetbrains.com/issue/KT-51379    exclusiveContent {      forRepository {        ivy(&quot;https://download.jetbrains.com/kotlin/native/builds&quot;) {          name = &quot;Kotlin Native&quot;          patternLayout {            listOf(                &quot;macos-x86_64&quot;,                &quot;macos-aarch64&quot;,                &quot;osx-x86_64&quot;,                &quot;osx-aarch64&quot;,                &quot;linux-x86_64&quot;,                &quot;windows-x86_64&quot;,              )              .forEach { os ->                listOf(&quot;dev&quot;, &quot;releases&quot;).forEach { stage ->                  artifact(&quot;$stage/[revision]/$os/[artifact]-[revision].[ext]&quot;)                }              }          }          metadataSources { artifact() }        }      }      filter { includeModuleByRegex(&quot;.*&quot;, &quot;.*kotlin-native-prebuilt.*&quot;) }    }    exclusiveContent {      forRepository {        ivy(&quot;https://nodejs.org/dist/&quot;) {          name = &quot;Node Distributions at $url&quot;          patternLayout { artifact(&quot;v[revision]/[artifact](-v[revision]-[classifier]).[ext]&quot;) }          metadataSources { artifact() }          content { includeModule(&quot;org.nodejs&quot;, &quot;node&quot;) }        }      }      filter { includeGroup(&quot;org.nodejs&quot;) }    }    exclusiveContent {      forRepository {        ivy(&quot;https://github.com/yarnpkg/yarn/releases/download&quot;) {          name = &quot;Yarn Distributions at $url&quot;          patternLayout { artifact(&quot;v[revision]/[artifact](-v[revision]).[ext]&quot;) }          metadataSources { artifact() }          content { includeModule(&quot;com.yarnpkg&quot;, &quot;yarn&quot;) }        }      }      filter { includeGroup(&quot;com.yarnpkg&quot;) }    }  }}\"><div></div></button></div></figure></div>\n<p>If you’re not using a JavaScript target in your project, it should be safe to skip the NodeJS and Yarn repositories but it’s probably easier to keep it configured ahead of time in case you adopt JavaScript in the future.</p>\n<h2 id=\"conclusion\">Conclusion</h2>\n<p>The exact percentage improvement you can expect can vary depending on how many dependencies you have as well as how many repositories were previously declared and in what order, but you should most definitely see a noticeable difference consistently. These are the before and after numbers for a project I optimised for my day job.</p>\n<h3 id=\"before\">Before</h3>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/before-fixes.V7ZgPWHx.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/before-fixes.V7ZgPWHx.webp\" data-pswp-width=\"1562\" data-pswp-height=\"374\" data-pswp-caption=\"The Android Studio Dependency Sync window, showing a total sync duration of 5 minutes and 56 seconds of which 1 minute and 30 seconds went into failed network requests\" aria-label=\"The Android Studio Dependency Sync window, showing a total sync duration of 5 minutes and 56 seconds of which 1 minute and 30 seconds went into failed network requests\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/before-fixes.V7ZgPWHx.webp\" alt=\"\" loading=\"lazy\" decoding=\"async\" width=\"1562\" height=\"374\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">The Android Studio Dependency Sync window, showing a total sync duration of 5 minutes and 56 seconds of which 1 minute and 30 seconds went into failed network requests</figcaption></figure>\n<h3 id=\"after\">After</h3>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/after-fixes.CAZIG7hs.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/after-fixes.CAZIG7hs.webp\" data-pswp-width=\"1562\" data-pswp-height=\"374\" data-pswp-caption=\"The Android Studio Dependency Sync window, now showing the total sync taking only 3 minutes and 17 seconds with 0 failed requests\" aria-label=\"The Android Studio Dependency Sync window, now showing the total sync taking only 3 minutes and 17 seconds with 0 failed requests\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/after-fixes.CAZIG7hs.webp\" alt=\"\" loading=\"lazy\" decoding=\"async\" width=\"1562\" height=\"374\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a><figcaption data-astro-cid-xekddyko=\"\">The Android Studio Dependency Sync window, now showing the total sync taking only 3 minutes and 17 seconds with 0 failed requests</figcaption></figure>\n<p>Like and subscribe, and hit that notification bell so you don’t miss my next post some time within this decade (hopefully).</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/tips-and-tricks-for-using-renovate/",
      "url": "https://msfjarvis.dev/posts/tips-and-tricks-for-using-renovate/",
      "title": "Tips and tricks for using Renovate",
      "date_published": "2023-01-17T19:32:18.000Z",
      "summary": "Renovate is an extremely powerful tool for keeping your dependencies up-to-date, and its flexibility is often left unexplored. I'm hoping to change that",
      "content_html": "<p><a href=\"https://www.mend.io/free-developer-tools/renovate/\">Mend Renovate</a> is a free to use dependency update management service powered by the open-source <a href=\"https://github.com/renovatebot/renovate\">renovate</a>, and is a compelling alternative to GitHub’s blessed solution for this problem space: <a href=\"https://docs.github.com/en/code-security/dependabot\">Dependabot</a>. Renovate offers a significantly larger suite of supported language ecosystems compared to Dependabot as well as fine-grained control over where it finds dependencies, how it chooses updated versions, and a lot more. TL;DR: Renovate is a massive upgrade over Dependabot and you should evaluate it if <em>any</em> aspect of Dependabot has caused you grief, there’s a good chance Renovate does it better.</p>\n<p>I’m collecting some tips here about “fancy” things I’ve done using Renovate that may be helpful to other folks. You’ll be able to find more details about all of these in their very high quality docs at <a href=\"https://docs.renovatebot.com/\">docs.renovatebot.com</a>.</p>\n<h2 id=\"disabling-updates-for-individual-packages\">Disabling updates for individual packages</h2>\n<p>There are times where you’re sticking with an older version of a package (temporarily or otherwise) and you just don’t want to see PRs bumping it, wasting CI resources for an upgrade that will probably fail and is definitely not going to be merged. Renovate offers a convenient way to do this:</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"json\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">packageRules</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">managers</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">gradle</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">],</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">packagePatterns</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">^com.squareup.okhttp3</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">],</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">enabled</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">false</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">]</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"{  &quot;packageRules&quot;: [    {      &quot;managers&quot;: [&quot;gradle&quot;],      &quot;packagePatterns&quot;: [&quot;^com.squareup.okhttp3&quot;],      &quot;enabled&quot;: false    }  ]}\"><div></div></button></div></figure></div>\n<h2 id=\"grouping-updates-together\">Grouping updates together</h2>\n<p>Renovate already includes preset configurations for <a href=\"https://github.com/renovatebot/renovate/blob/b4d1ad8e5210017a3550c9da4342b0953a70330a/lib/config/presets/internal/monorepo.ts\">monorepos</a> that publish multiple packages with identical versions, but you can also easily add more of your own. As an example, here’s how you can combine updates of the serde crate and its derive macro.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"json\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">packageRules</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">managers</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">cargo</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">],</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">matchPackagePatterns</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">serde</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">serde_derive</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">],</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">groupName</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">serde</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">]</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"{  &quot;packageRules&quot;: [    {      &quot;managers&quot;: [&quot;cargo&quot;],      &quot;matchPackagePatterns&quot;: [&quot;serde&quot;, &quot;serde_derive&quot;],      &quot;groupName&quot;: &quot;serde&quot;    }  ]}\"><div></div></button></div></figure></div>\n<h2 id=\"set-a-semver-range-for-upgrades\">Set a semver range for upgrades</h2>\n<p>Sometimes there are cases where you may need to set an upper bound on a package dependency to avoid breaking changes or regressions. Renovate offers intuitive support for the same.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"json\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">packageRules</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">matchPackageNames</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">com.android.tools.build:gradle</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">],</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">allowedVersions</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">&lt;=7.4.0</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">]</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"{  &quot;packageRules&quot;: [    {      &quot;matchPackageNames&quot;: [&quot;com.android.tools.build:gradle&quot;],      &quot;allowedVersions&quot;: &quot;<=7.4.0&quot;    }  ]}\"><div></div></button></div></figure></div>\n<h2 id=\"supporting-non-standard-dependency-declarations\">Supporting non-standard dependency declarations</h2>\n<p>Dependency versions are sometimes specified without their package names, for example in config files. These cannot be automatically detected by Renovate, but you can use a regular expression to teach it how to identify these dependencies.</p>\n<p>For example, you can specify the version of Hugo to build your Netlify site with in the <code>netlify.toml</code> file in your repository.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"toml\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span><span style=\"--0:#4C4F69;--1:#FABD2F\">build</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#4C4F69;--1:#FABD2F\">environment</span><span style=\"--0:#5D5F6E;--1:#B4A694\">]</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#91AFA3\">HUGO_VERSION</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#B4A694\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">0.109.0</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"[build.environment]HUGO_VERSION = &quot;0.109.0&quot;\"><div></div></button></div></figure></div>\n<p>This is how the relevant configuration might look like with Renovate</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"json\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">regexManagers</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">description</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">Update Hugo version in Netlify config</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">fileMatch</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">.toml$</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">],</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">matchStrings</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">HUGO_VERSION = </span><span style=\"--0:#8D477A;--1:#D995A8\">\\\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">(?&lt;currentValue&gt;.*?)</span><span style=\"--0:#8D477A;--1:#D995A8\">\\\"</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">],</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">depNameTemplate</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">gohugoio/hugo</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">datasourceTemplate</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">github-releases</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">]</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"{  &quot;regexManagers&quot;: [    {      &quot;description&quot;: &quot;Update Hugo version in Netlify config&quot;,      &quot;fileMatch&quot;: [&quot;.toml$&quot;],      &quot;matchStrings&quot;: [&quot;HUGO_VERSION = \\&quot;(?<currentValue>.*?)\\&quot;&quot;],      &quot;depNameTemplate&quot;: &quot;gohugoio/hugo&quot;,      &quot;datasourceTemplate&quot;: &quot;github-releases&quot;    }  ]}\"><div></div></button></div></figure></div>\n<p>You can read more about Regex Managers <a href=\"https://docs.renovatebot.com/modules/manager/regex/\">here</a>.</p>\n<h2 id=\"making-your-github-actions-usage-more-secure\">Making your GitHub Actions usage more secure</h2>\n<p>According to GitHub’s <a href=\"https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions\">official recommendations</a>, you should be using exact commit SHAs instead of tags for third-party actions. However, this is a pain to do manually. Instead, allow Renovate to manage it for you!</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"json\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">extends</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">config:base</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">:dependencyDashboard</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">helpers:pinGitHubActionDigests</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">]</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"{  &quot;extends&quot;: [    &quot;config:base&quot;,    &quot;:dependencyDashboard&quot;,    &quot;helpers:pinGitHubActionDigests&quot;  ]}\"><div></div></button></div></figure></div>\n<h2 id=\"automatically-merging-compatible-updates\">Automatically merging compatible updates</h2>\n<p>Every person with a JavaScript project has definitely loved getting 20 PRs from Dependabot about arbitrary transitive dependencies that they didn’t even realise they had. With Renovate, that pain can also be automated away if you have a robust enough test suite to permit automatic merging of minor updates.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"json\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">automergeType</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">branch</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">packageRules</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">description</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">Automerge non-major updates</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">matchUpdateTypes</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">minor</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">patch</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">digest</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">lockFileMaintenance</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">],</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">automerge</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">true</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">]</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"{  &quot;automergeType&quot;: &quot;branch&quot;,  &quot;packageRules&quot;: [    {      &quot;description&quot;: &quot;Automerge non-major updates&quot;,      &quot;matchUpdateTypes&quot;: [&quot;minor&quot;, &quot;patch&quot;, &quot;digest&quot;, &quot;lockFileMaintenance&quot;],      &quot;automerge&quot;: true    }  ]}\"><div></div></button></div></figure></div>\n<p>With this configuration, Renovate will push compatible updates to <code>renovate/$depName</code> branches and merge it automatically to your main branch if CI runs on the branch and passes. To make that happen, you will also need to update your GitHub Actions workflows.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">name: Run tests</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">on:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">pull_request:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">branches:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">- main</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">push:</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">branches:</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">      </span></span><span style=\"--1:#EBDBB2\">- renovate/**</span></span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"name: Run testson:  pull_request:    branches:      - main  push:    branches:      - renovate/**\"><div></div></button></div></figure></div>\n<h2 id=\"closing-notes\">Closing notes</h2>\n<p>This list currently consists exclusively of things I’ve used in my own projects. There is way more you can achieve with Renovate, and I recommend going through the docs at <a href=\"https://docs.renovatebot.com/\">docs.renovatebot.com</a> to find any useful knobs for the language ecosystem you wish to use it with. If you come across something interesting not covered here, let me know either below or on Mastodon at <a href=\"https://fantastic.earth/@msfjarvis\">@msfjarvis@fantastic.earth</a>!</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/writing-your-own-nix-flake-checks/",
      "url": "https://msfjarvis.dev/posts/writing-your-own-nix-flake-checks/",
      "title": "Writing your own Nix Flake checks",
      "date_published": "2022-12-18T06:30:00.000Z",
      "summary": "Quick how-to for writing ad-hoc checks for your own Nix Flakes",
      "content_html": "<h2 id=\"preface\">Preface</h2>\n<p>Ever since discovering <a href=\"https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake-check.html\">nix(3) flake check</a> from <a href=\"https://github.com/ipetkov/crane\">crane</a> (wonderful tool btw, highly recommend it if you’re building Rust things), I’ve wanted to be able to quickly write my own flake checks. Unfortunately, as with everything Nix, dummy-friendly documentation was hard to come by so I started trying out a bunch of things until I ended up with something that worked, which I’ll share below.</p>\n<h2 id=\"the-premise\">The premise</h2>\n<p>I had been using a basic shell script with a <code>nix-shell</code> shebang for a while to run formatters on my scripts repo and while it worked, <code>nix-shell</code> startup is fairly slow and it just wasn’t cutting it for me. So I decided to try porting it to <code>nix flake check</code> which would benefit from evaluation caching and be faster while removing the overhead of <code>nix-shell</code> from the utility script.</p>\n<h2 id=\"the-thing-youre-here-for\">The thing you’re here for</h2>\n<p>Like everything in Nix, the checks needed to be derivations that Nix will build and run the respective <code>checkPhase</code> of. So naively, I put together this to run the <a href=\"https://github.com/kamadorueda/alejandra\">alejandra</a> Nix formatter, <a href=\"https://github.com/mvdan/sh\">shfmt</a> to format shell scripts and <a href=\"https://shellcheck.net/\">shellcheck</a> to lint them:</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"nix\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#91AFA3\">outputs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#E6928E\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#AD343E;--0fs:italic;--1:#91AFA3\">self</span><span style=\"--0:#116B70;--1:#8EC07C\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#AD343E;--0fs:italic;--1:#91AFA3\">nixpkgs</span><span style=\"--0:#116B70;--1:#8EC07C\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#AD343E;--0fs:italic;--1:#91AFA3\">flake-utils</span><span style=\"--0:#116B70;--1:#8EC07C\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69\">  </span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">flake-utils</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">lib</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">eachDefaultSystem</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">(</span><span style=\"--0:#AD343E;--0fs:italic;--1:#91AFA3\">system</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">let</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#FABD2F\">pkgs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--1:#EBDBB2\"><span style=\"--0:#4C4F69\"> </span><span style=\"--0:#1A57D1;--0fs:italic\">import</span><span style=\"--0:#4C4F69\"> </span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">nixpkgs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span><span style=\"--0:#7B34D9;--1:#FC8678\">inherit</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--1:#FABD2F\">system</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;};</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#FABD2F\">files</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">pkgs</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">lib</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">concatStringsSep</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Individual shell scripts from the repository</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">];</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#FABD2F\">fmt-check</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">pkgs</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">stdenv</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">mkDerivation</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--1:#FABD2F\">name</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">fmt-check</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--1:#FABD2F\">src</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#B8BB26\">./.</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--1:#FABD2F\">doCheck</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--1:#D995A8\">true</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--1:#FABD2F\">nativeBuildInputs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">with</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">pkgs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">; </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span><span style=\"--0:#4C4F69;--1:#91AFA3\">alejandra</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">shellcheck</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">shfmt</span><span style=\"--0:#5D5F6E;--1:#B4A694\">];</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--1:#FABD2F\">checkPhase</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">''</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">        </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">shfmt -d -s -i 2 -ci </span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">files</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">        </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">alejandra -c .</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">        </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">shellcheck -x </span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">files</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E\">      </span></span><span style=\"--0:#2C6E1E;--1:#B4A694\">''</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">};</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">in</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#FABD2F\">checks</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span><span style=\"--0:#7B34D9;--1:#FC8678\">inherit</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--1:#FABD2F\">fmt-check</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;};</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">})</span><span style=\"--0:#4C4F69;--1:#E6928E\">;</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"outputs = {  self,  nixpkgs,  flake-utils,}:  flake-utils.lib.eachDefaultSystem (system: let    pkgs = import nixpkgs {inherit system;};    files = pkgs.lib.concatStringsSep &quot; &quot; [      # Individual shell scripts from the repository    ];    fmt-check = pkgs.stdenv.mkDerivation {      name = &quot;fmt-check&quot;;      src = ./.;      doCheck = true;      nativeBuildInputs = with pkgs; [alejandra shellcheck shfmt];      checkPhase = ''        shfmt -d -s -i 2 -ci ${files}        alejandra -c .        shellcheck -x ${files}      '';    };  in {    checks = {inherit fmt-check;};  });\"><div></div></button></div></figure></div>\n<p>I needed a space separated list of my shell scripts to pass to shfmt and shellcheck, so I used a library function from nixpkgs called <code>concatStringsSep</code> that takes a list, and concatenates it together with the given separator. That’s the <code>files</code> binding declared in the snippet above.</p>\n<p>Here I ran into my first problem: Nix expects every derivation to generate an output which meant this doesn’t actually build.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plaintext\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">➜ nix flake check</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">error: flake attribute 'checks.fmt-check.outPath' is not a derivation</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"➜ nix flake checkerror: flake attribute 'checks.fmt-check.outPath' is not a derivation\"><div></div></button></div></figure></div>\n<p>There’s been <a href=\"https://github.com/NixOS/nixpkgs/issues/16182\">some discussion</a> about this but the TL;DR is that <code>mkDerivation</code> must produce an output. So I tried to cheat around this requirement by faking an output.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">diff --git flake.nix flake.nix</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">index b7fef3b99110..a531a30ad88e 100644</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--1:#B4A694\">---</span><span style=\"--0:#1A57D1;--1:#EBDBB2\"> flake.nix</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--1:#B4A694\">+++</span><span style=\"--0:#1A57D1;--1:#EBDBB2\"> flake.nix</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">@@</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> -18,6 +18,7 </span><span style=\"--0:#5D5F6E;--1:#B4A694\">@@</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">       </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">];</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">       </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">fmt-check = pkgs.stdenv.mkDerivation {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">         </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">name = \"fmt-check\";</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">+</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">        dontBuild = true;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">         </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">src = ./.;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">         </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">doCheck = true;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">         </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">nativeBuildInputs = with pkgs; [alejandra shellcheck shfmt];</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">         </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">checkPhase = ''</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">@@</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> -25,6 +26,11 </span><span style=\"--0:#5D5F6E;--1:#B4A694\">@@</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">           </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">alejandra -c .</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">           </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">shellcheck -x ${files}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">         </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">'';</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">+</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">        installPhase = ''</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">+</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">          mkdir \"$out\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">+</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">        '';</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">       </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">};</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">     </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">in {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">       </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">checks = {inherit fmt-check;};</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"diff --git flake.nix flake.nixindex b7fef3b99110..a531a30ad88e 100644--- flake.nix+++ flake.nix@@ -18,6 +18,7 @@       ];       fmt-check = pkgs.stdenv.mkDerivation {         name = &quot;fmt-check&quot;;+        dontBuild = true;         src = ./.;         doCheck = true;         nativeBuildInputs = with pkgs; [alejandra shellcheck shfmt];         checkPhase = ''@@ -25,6 +26,11 @@           alejandra -c .           shellcheck -x ${files}         '';+        installPhase = ''+          mkdir &quot;$out&quot;+        '';       };     in {       checks = {inherit fmt-check;};\"><div></div></button></div></figure></div>\n<p><code>dontBuild</code> does exactly what you’d think and makes Nix not execute the <code>buildPhase</code> of the derivation, and the <code>mkdir $out</code> in the <code>installPhase</code> generates the output directory Nix was looking for which is still valid even if completely empty.</p>\n<p>You can make this slightly faster by using a smaller stdenv that won’t pull in a compiler toolchain or be rebuilt when said toolchain is updated:</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">diff --git flake.nix flake.nix</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">index 7ce7a2ba80f8..b69db13fbc6d 100644</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--1:#B4A694\">---</span><span style=\"--0:#1A57D1;--1:#EBDBB2\"> flake.nix</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--1:#B4A694\">+++</span><span style=\"--0:#1A57D1;--1:#EBDBB2\"> flake.nix</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">@@</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> -16,7 +16,7 </span><span style=\"--0:#5D5F6E;--1:#B4A694\">@@</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">       </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">files = pkgs.lib.concatStringsSep \" \" [</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">         </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\"># bunch of shell scripts since I didn't have an extension I could glob against</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">       </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">];</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">-</span><span style=\"--0:#C00E34;--1:#E49663\">      fmt-check = pkgs.stdenv.mkDerivation {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">+</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">      fmt-check = pkgs.stdenvNoCC.mkDerivation {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">         </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">name = \"fmt-check\";</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">         </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">dontBuild = true;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">         </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">src = ./.;</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"diff --git flake.nix flake.nixindex 7ce7a2ba80f8..b69db13fbc6d 100644--- flake.nix+++ flake.nix@@ -16,7 +16,7 @@       files = pkgs.lib.concatStringsSep &quot; &quot; [         # bunch of shell scripts since I didn't have an extension I could glob against       ];-      fmt-check = pkgs.stdenv.mkDerivation {+      fmt-check = pkgs.stdenvNoCC.mkDerivation {         name = &quot;fmt-check&quot;;         dontBuild = true;         src = ./.;\"><div></div></button></div></figure></div>\n<h2 id=\"the-final-result\">The final result</h2>\n<p>This is what the flake looked like for me after all this</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"nix\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">description</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">A very basic flake</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">inputs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#FABD2F\">nixpkgs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#1A57D1;--1:#FABD2F\">url</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">github:nixos/nixpkgs/nixpkgs-unstable</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#FABD2F\">flake-utils</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#1A57D1;--1:#FABD2F\">url</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">github:numtide/flake-utils/main</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">};</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">outputs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#AD343E;--0fs:italic;--1:#91AFA3\">self</span><span style=\"--0:#116B70;--1:#8EC07C\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#AD343E;--0fs:italic;--1:#91AFA3\">nixpkgs</span><span style=\"--0:#116B70;--1:#8EC07C\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#AD343E;--0fs:italic;--1:#91AFA3\">flake-utils</span><span style=\"--0:#116B70;--1:#8EC07C\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69\">    </span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">flake-utils</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">lib</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">eachDefaultSystem</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">(</span><span style=\"--0:#AD343E;--0fs:italic;--1:#91AFA3\">system</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">let</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--1:#FABD2F\">pkgs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--1:#EBDBB2\"><span style=\"--0:#4C4F69\"> </span><span style=\"--0:#1A57D1;--0fs:italic\">import</span><span style=\"--0:#4C4F69\"> </span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">nixpkgs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span><span style=\"--0:#7B34D9;--1:#FC8678\">inherit</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--1:#FABD2F\">system</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;};</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--1:#FABD2F\">files</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">pkgs</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">lib</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">concatStringsSep</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Individual shell scripts from the repository</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">];</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--1:#FABD2F\">fmt-check</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">pkgs</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">stdenvNoCC</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">mkDerivation</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--1:#FABD2F\">name</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">fmt-check</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--1:#FABD2F\">dontBuild</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--1:#D995A8\">true</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--1:#FABD2F\">src</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#B8BB26\">./.</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--1:#FABD2F\">doCheck</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--1:#D995A8\">true</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--1:#FABD2F\">nativeBuildInputs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">with</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">pkgs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">; </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span><span style=\"--0:#4C4F69;--1:#91AFA3\">alejandra</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">shellcheck</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">shfmt</span><span style=\"--0:#5D5F6E;--1:#B4A694\">];</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--1:#FABD2F\">checkPhase</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">''</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">shfmt -d -s -i 2 -ci </span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">files</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">alejandra -c .</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">shellcheck -x </span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">files</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E\">        </span></span><span style=\"--0:#2C6E1E;--1:#B4A694\">''</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--1:#FABD2F\">installPhase</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">''</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">mkdir \"$out\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E\">        </span></span><span style=\"--0:#2C6E1E;--1:#B4A694\">''</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">};</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">in</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--1:#FABD2F\">checks</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span><span style=\"--0:#7B34D9;--1:#FC8678\">inherit</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--1:#FABD2F\">fmt-check</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;};</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">});</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"{  description = &quot;A very basic flake&quot;;  inputs = {    nixpkgs.url = &quot;github:nixos/nixpkgs/nixpkgs-unstable&quot;;    flake-utils.url = &quot;github:numtide/flake-utils/main&quot;;  };  outputs = {    self,    nixpkgs,    flake-utils,  }:    flake-utils.lib.eachDefaultSystem (system: let      pkgs = import nixpkgs {inherit system;};      files = pkgs.lib.concatStringsSep &quot; &quot; [        # Individual shell scripts from the repository      ];      fmt-check = pkgs.stdenvNoCC.mkDerivation {        name = &quot;fmt-check&quot;;        dontBuild = true;        src = ./.;        doCheck = true;        nativeBuildInputs = with pkgs; [alejandra shellcheck shfmt];        checkPhase = ''          shfmt -d -s -i 2 -ci ${files}          alejandra -c .          shellcheck -x ${files}        '';        installPhase = ''          mkdir &quot;$out&quot;        '';      };    in {      checks = {inherit fmt-check;};    });}\"><div></div></button></div></figure></div>\n<p>It’s probably not idiomatic Nix (for some definition of idiomatic) but the entire thing has been a trial and error anyway so :shrug:</p>\n<p>I’m very much a noob when it comes to Nix so any feedback is very welcome and appreciated!</p>\n<h3 id=\"alternative-solution-june-2024-update\">Alternative solution (June 2024 update)</h3>\n<p>The above ‘hack’ can also be changed to use <a href=\"https://nixos.org/manual/nixpkgs/stable/#trivial-builder-runCommand\">pkgs.runCommand</a> which I recently learned about.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"nix\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">description</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">A very basic flake</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">inputs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#FABD2F\">nixpkgs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#1A57D1;--1:#FABD2F\">url</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">github:nixos/nixpkgs/nixpkgs-unstable</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#FABD2F\">flake-utils</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.</span><span style=\"--0:#1A57D1;--1:#FABD2F\">url</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">github:numtide/flake-utils/main</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">};</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#FABD2F\">outputs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#AD343E;--0fs:italic;--1:#91AFA3\">self</span><span style=\"--0:#116B70;--1:#8EC07C\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#AD343E;--0fs:italic;--1:#91AFA3\">nixpkgs</span><span style=\"--0:#116B70;--1:#8EC07C\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#AD343E;--0fs:italic;--1:#91AFA3\">flake-utils</span><span style=\"--0:#116B70;--1:#8EC07C\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69\">    </span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">flake-utils</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">lib</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">eachDefaultSystem</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">(</span><span style=\"--0:#AD343E;--0fs:italic;--1:#91AFA3\">system</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">let</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--1:#FABD2F\">pkgs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--1:#EBDBB2\"><span style=\"--0:#4C4F69\"> </span><span style=\"--0:#1A57D1;--0fs:italic\">import</span><span style=\"--0:#4C4F69\"> </span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">nixpkgs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span><span style=\"--0:#7B34D9;--1:#FC8678\">inherit</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--1:#FABD2F\">system</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;};</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--1:#FABD2F\">files</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">pkgs</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">lib</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">concatStringsSep</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Individual shell scripts from the repository</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">];</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Variant of runCommand intended for commands</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># that run quickly and will be slowed down by</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># the network round-trip.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--1:#FABD2F\">fmt-check</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">pkgs</span><span style=\"--0:#116B70;--1:#8EC07C\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">runCommandLocal</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">fmt-check</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--1:#FABD2F\">src</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#B8BB26\">./.</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--1:#FABD2F\">nativeBuildInputs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">with</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">pkgs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">; </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span><span style=\"--0:#4C4F69;--1:#91AFA3\">alejandra</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">shellcheck</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">shfmt</span><span style=\"--0:#5D5F6E;--1:#B4A694\">];</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">''</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">shfmt -d -s -i 2 -ci </span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">files</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">alejandra -c .</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">shellcheck -x </span><span style=\"--0:#5D5F6E;--1:#B4A694\">${</span><span style=\"--0:#4C5AA8;--1:#91AFA3\">files</span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E;--1:#B8BB26\">          </span></span><span style=\"--0:#2C6E1E;--1:#B8BB26\">mkdir \"$out\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#2C6E1E\">        </span></span><span style=\"--0:#2C6E1E;--1:#B4A694\">''</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">in</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--1:#FABD2F\">checks</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span><span style=\"--0:#7B34D9;--1:#FC8678\">inherit</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--1:#FABD2F\">fmt-check</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;};</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">});</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"{  description = &quot;A very basic flake&quot;;  inputs = {    nixpkgs.url = &quot;github:nixos/nixpkgs/nixpkgs-unstable&quot;;    flake-utils.url = &quot;github:numtide/flake-utils/main&quot;;  };  outputs = {    self,    nixpkgs,    flake-utils,  }:    flake-utils.lib.eachDefaultSystem (system: let      pkgs = import nixpkgs {inherit system;};      files = pkgs.lib.concatStringsSep &quot; &quot; [        # Individual shell scripts from the repository      ];      # Variant of runCommand intended for commands      # that run quickly and will be slowed down by      # the network round-trip.      fmt-check = pkgs.runCommandLocal &quot;fmt-check&quot; {        src = ./.;        nativeBuildInputs = with pkgs; [alejandra shellcheck shfmt];      } ''          shfmt -d -s -i 2 -ci ${files}          alejandra -c .          shellcheck -x ${files}          mkdir &quot;$out&quot;        '';    in {      checks = {inherit fmt-check;};    });}\"><div></div></button></div></figure></div>"
    },
    {
      "id": "https://msfjarvis.dev/posts/mastodon-on-your-own-domain-without-hosting-a-server-netlify-edition/",
      "url": "https://msfjarvis.dev/posts/mastodon-on-your-own-domain-without-hosting-a-server-netlify-edition/",
      "title": "Mastodon on your own domain without hosting a server, Netlify edition",
      "date_published": "2022-11-16T06:30:00.000Z",
      "summary": "A quick and easy way of creating a Fediverse identity on your own domain without an ActivityPub server",
      "content_html": "<h2 id=\"preface\">Preface</h2>\n<p>I recently came across <a href=\"https://blog.maartenballiauw.be/post/2022/11/05/mastodon-own-donain-without-hosting-server.html\">a blog post</a> from <a href=\"https://mastodon.online/@maartenballiauw\">Maarten Balliauw</a> that explained how they had managed to create an ActivityPub compatible identity for themselves, without hosting Mastodon or any other ActivityPub server.</p>\n<p>I recommend going to their blog and reading the whole thing, but here’s a TL;DR</p>\n<ul>\n<li><a href=\"https://activitypub.rocks/\">ActivityPub</a> has the notion of an “actor” that sends messages</li>\n<li>This “actor” must be discoverable via a protocol called <a href=\"https://webfinger.net\">WebFinger</a></li>\n<li>WebFinger is ridiculously easy to implement</li>\n</ul>\n<p>For all practical purposes, WebFinger is essentially a JSON document that is served at <code>/.well-known/webfinger</code> from a domain and is used to identify “actors” across the Fediverse.</p>\n<p>Maarten’s approach to implementing this was to simply place the JSON document at <code>/.well-known/webfinger</code> on their domain <code>balliauw.be</code>, which allowed <code>@maarten@balliauw.be</code> to become a WebFinger-compatible identity that can be searched for on Mastodon and will return their actual <code>@maartenballiauw.be@mastodon.online</code> profile.</p>\n<p>Maarten did however note that since they’re relying on static hosting, they’re unable to restrict what identities they can enforce as valid, and thus a search for <code>@anything@balliauw.be</code> will also return their <code>mastodon.online</code> identity.</p>\n<h2 id=\"the-implementation\">The implementation</h2>\n<p>I wanted to also set up something like this, but without the limitation Maarten had run into. Since my website runs on Netlify, I decided to try out using an <a href=\"https://docs.netlify.com/edge-functions/overview/\">Edge Function</a> to build this up.</p>\n<p>Similar to Maarten, I first obtained my current Fediverse identity from the Mastodon server I am on: <a href=\"https://fantastic.earth\">fantastic.earth</a>.</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"json\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">➜ curl -s https:</span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">//fantastic.earth/.well-known/webfinger?resource=acct:msfjarvis@fantastic.earth | jq .</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">subject</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">acct:msfjarvis@fantastic.earth</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">aliases</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">https://fantastic.earth/@msfjarvis</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">https://fantastic.earth/users/msfjarvis</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">],</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">links</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">rel</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">http://webfinger.net/rel/profile-page</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">type</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">text/html</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">href</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">https://fantastic.earth/@msfjarvis</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">},</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">rel</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">self</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">type</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">application/activity+json</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">href</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">https://fantastic.earth/users/msfjarvis</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">},</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">rel</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">http://ostatus.org/schema/1.0/subscribe</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">template</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">https://fantastic.earth/authorize_interaction?uri={uri}</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">]</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"➜ curl -s https://fantastic.earth/.well-known/webfinger?resource=acct:msfjarvis@fantastic.earth | jq .{  &quot;subject&quot;: &quot;acct:msfjarvis@fantastic.earth&quot;,  &quot;aliases&quot;: [    &quot;https://fantastic.earth/@msfjarvis&quot;,    &quot;https://fantastic.earth/users/msfjarvis&quot;  ],  &quot;links&quot;: [    {      &quot;rel&quot;: &quot;http://webfinger.net/rel/profile-page&quot;,      &quot;type&quot;: &quot;text/html&quot;,      &quot;href&quot;: &quot;https://fantastic.earth/@msfjarvis&quot;    },    {      &quot;rel&quot;: &quot;self&quot;,      &quot;type&quot;: &quot;application/activity+json&quot;,      &quot;href&quot;: &quot;https://fantastic.earth/users/msfjarvis&quot;    },    {      &quot;rel&quot;: &quot;http://ostatus.org/schema/1.0/subscribe&quot;,      &quot;template&quot;: &quot;https://fantastic.earth/authorize_interaction?uri={uri}&quot;    }  ]}\"><div></div></button></div></figure></div>\n<p>With this in hand, now we can get started on wiring this up into our website.</p>\n<p>First, create an Edge Function using the Netlify CLI. Here’s the options I chose.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plaintext\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">➜ yarn exec ntl functions:create --name webfinger</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">? Select the type of function you'd like to create: Edge function (Deno)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">? Select the language of your function: TypeScript</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">? Pick a template: typescript-json</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">? Name your function: webfinger</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">◈ Creating function webfinger</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">◈ Created netlify/edge-functions/webfinger/webfinger.ts</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">? What route do you want your edge function to be invoked on?: /.well-known/webfinger</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">◈ Function 'webfinger' registered for route `/.well-known/webfinger`. To change, edit your `netlify.toml` file.</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"➜ yarn exec ntl functions:create --name webfinger? Select the type of function you'd like to create: Edge function (Deno)? Select the language of your function: TypeScript? Pick a template: typescript-json? Name your function: webfinger◈ Creating function webfinger◈ Created netlify/edge-functions/webfinger/webfinger.ts? What route do you want your edge function to be invoked on?: /.well-known/webfinger◈ Function 'webfinger' registered for route `/.well-known/webfinger`. To change, edit your `netlify.toml` file.\"><div></div></button></div></figure></div>\n<p>Next, add the following code to the TypeScript file just created for you. I’ve added comments inline to explain what each part of the code does so you can customize it according to your needs.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"typescript\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Netlify Edge Functions run on Deno (https://deno.land), so imports use URLs rather than package names.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">import</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">Status</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">from</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">https://deno.land/std@0.136.0/http/http_status.ts</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">import</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">type</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">Context</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">from</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">https://edge.netlify.com</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">export</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">default</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">async</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">(</span><span style=\"--0:#AD343E;--0fs:italic;--1:#91AFA3\">request</span><span style=\"--0:#116B70;--1:#8EC07C\">:</span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\"> Request</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#AD343E;--0fs:italic;--1:#91AFA3\">context</span><span style=\"--0:#116B70;--1:#8EC07C\">:</span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\"> Context</span><span style=\"--0:#5D5F6E;--1:#B4A694\">)</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#FE8929\">=&gt;</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// We obtain the value of the 'resource' query parameter so that we</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// can ensure a response is only sent for the identity we want.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FE8929\">const</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">url</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--0fw:bold;--1:#FE8929\">new</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">URL</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#4C4F69;--1:#91AFA3\">request</span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">url</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FE8929\">const</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">resourceParam</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">url</span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">searchParams</span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">get</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">resource</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">if</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (</span><span style=\"--0:#4C4F69;--1:#91AFA3\">resourceParam</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">===</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#D995A8\">null</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">return</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">context</span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">json</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">        </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">error</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">No 'resource' query parameter was provided</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">},</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">        </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">status</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">Status</span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">BadRequest</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">},</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// I want to be searchable as `@harsh@msfjarvis.dev`, so I only</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// allow requests that set the resource query param to this value.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">else</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">if</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (</span><span style=\"--0:#4C4F69;--1:#91AFA3\">resourceParam</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">!==</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">acct:harsh@msfjarvis.dev</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">return</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">context</span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">json</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">        </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">error</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">An invalid identity was requested</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">},</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">        </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">status</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">Status</span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">BadRequest</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">},</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">else</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Here's the JSON object we got earlier</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">return</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">context</span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">json</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">subject</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">acct:msfjarvis@fantastic.earth</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">aliases</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> [</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">https://fantastic.earth/@msfjarvis</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">https://fantastic.earth/users/msfjarvis</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">]</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">links</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> [</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">          </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">rel</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">http://webfinger.net/rel/profile-page</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">          </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">type</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">text/html</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">          </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">href</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">https://fantastic.earth/@msfjarvis</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#5D5F6E;--1:#B4A694\">},</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">          </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">rel</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">self</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">          </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">type</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">application/activity+json</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">          </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">href</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">https://fantastic.earth/users/msfjarvis</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#5D5F6E;--1:#B4A694\">},</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">          </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">rel</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">http://ostatus.org/schema/1.0/subscribe</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">          </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">template</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">https://fantastic.earth/authorize_interaction?uri={uri}</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#5D5F6E;--1:#B4A694\">},</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">]</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">};</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"// Netlify Edge Functions run on Deno (https://deno.land), so imports use URLs rather than package names.import { Status } from &quot;https://deno.land/std@0.136.0/http/http_status.ts&quot;;import type { Context } from &quot;https://edge.netlify.com&quot;;export default async (request: Request, context: Context) => {  // We obtain the value of the 'resource' query parameter so that we  // can ensure a response is only sent for the identity we want.  const url = new URL(request.url);  const resourceParam = url.searchParams.get(&quot;resource&quot;);  if (resourceParam === null) {    return context.json(      {        error: &quot;No 'resource' query parameter was provided&quot;,      },      {        status: Status.BadRequest,      },    );    // I want to be searchable as `@harsh@msfjarvis.dev`, so I only    // allow requests that set the resource query param to this value.  } else if (resourceParam !== &quot;acct:harsh@msfjarvis.dev&quot;) {    return context.json(      {        error: &quot;An invalid identity was requested&quot;,      },      {        status: Status.BadRequest,      },    );  } else {    // Here's the JSON object we got earlier    return context.json({      subject: &quot;acct:msfjarvis@fantastic.earth&quot;,      aliases: [        &quot;https://fantastic.earth/@msfjarvis&quot;,        &quot;https://fantastic.earth/users/msfjarvis&quot;,      ],      links: [        {          rel: &quot;http://webfinger.net/rel/profile-page&quot;,          type: &quot;text/html&quot;,          href: &quot;https://fantastic.earth/@msfjarvis&quot;,        },        {          rel: &quot;self&quot;,          type: &quot;application/activity+json&quot;,          href: &quot;https://fantastic.earth/users/msfjarvis&quot;,        },        {          rel: &quot;http://ostatus.org/schema/1.0/subscribe&quot;,          template: &quot;https://fantastic.earth/authorize_interaction?uri={uri}&quot;,        },      ],    });  }};\"><div></div></button></div></figure></div>\n<p>And that’s it! You can test it out as below to verify things work as expected.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"json\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">➜ curl -s https:</span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">//msfjarvis.dev/.well-known/webfinger | jq .</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">error</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">No 'resource' query parameter was provided</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">➜ curl -s https:</span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">//msfjarvis.dev/.well-known/webfinger?resource=acct:anything@msfjarvis.dev | jq .</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">error</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">An invalid identity was requested</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">➜ curl -s https:</span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">//msfjarvis.dev/.well-known/webfinger?resource=acct:harsh@msfjarvis.dev | jq .</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">subject</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">acct:msfjarvis@fantastic.earth</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">aliases</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">https://fantastic.earth/@msfjarvis</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">https://fantastic.earth/users/msfjarvis</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">],</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">links</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">rel</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">http://webfinger.net/rel/profile-page</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">type</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">text/html</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">href</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">https://fantastic.earth/@msfjarvis</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">},</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">rel</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">self</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">type</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">application/activity+json</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">href</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">https://fantastic.earth/users/msfjarvis</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">},</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">rel</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">http://ostatus.org/schema/1.0/subscribe</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">template</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">https://fantastic.earth/authorize_interaction?uri={uri}</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">]</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"➜ curl -s https://msfjarvis.dev/.well-known/webfinger | jq .{  &quot;error&quot;: &quot;No 'resource' query parameter was provided&quot;}➜ curl -s https://msfjarvis.dev/.well-known/webfinger?resource=acct:anything@msfjarvis.dev | jq .{  &quot;error&quot;: &quot;An invalid identity was requested&quot;}➜ curl -s https://msfjarvis.dev/.well-known/webfinger?resource=acct:harsh@msfjarvis.dev | jq .{  &quot;subject&quot;: &quot;acct:msfjarvis@fantastic.earth&quot;,  &quot;aliases&quot;: [    &quot;https://fantastic.earth/@msfjarvis&quot;,    &quot;https://fantastic.earth/users/msfjarvis&quot;  ],  &quot;links&quot;: [    {      &quot;rel&quot;: &quot;http://webfinger.net/rel/profile-page&quot;,      &quot;type&quot;: &quot;text/html&quot;,      &quot;href&quot;: &quot;https://fantastic.earth/@msfjarvis&quot;    },    {      &quot;rel&quot;: &quot;self&quot;,      &quot;type&quot;: &quot;application/activity+json&quot;,      &quot;href&quot;: &quot;https://fantastic.earth/users/msfjarvis&quot;    },    {      &quot;rel&quot;: &quot;http://ostatus.org/schema/1.0/subscribe&quot;,      &quot;template&quot;: &quot;https://fantastic.earth/authorize_interaction?uri={uri}&quot;    }  ]}\"><div></div></button></div></figure></div>\n<p>Thanks again to Maarten for doing the initial research for this and writing about it!</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/writing-paparazzi-tests-for-your-kotlin-multiplatform-projects/",
      "url": "https://msfjarvis.dev/posts/writing-paparazzi-tests-for-your-kotlin-multiplatform-projects/",
      "title": "Writing Paparazzi tests for your Kotlin Multiplatform projects",
      "date_published": "2022-06-26T06:30:00.000Z",
      "summary": "Paparazzi enables a radically faster and improved UI testing workflow, and using a small workaround we can bring that to our multiplatform Compose projects",
      "content_html": "<h2 id=\"introduction\">Introduction</h2>\n<p><a href=\"https://github.com/cashapp/paparazzi\">Paparazzi</a> is a Gradle plugin and library that enables writing UI tests for Android screens that run entirely on the JVM, without needing a physical device or emulator. This is massive, since it significantly increases the speed of UI tests as well as allows them to run on any CI system, not just ones using macOS or Linux with KVM enabled.</p>\n<p>Unfortunately, Paparazzi does not directly work with Kotlin Multiplatform projects so you cannot apply it to a KMP + Android module and start putting your tests in the <code>androidTest</code> source set (not to be confused with <code>androidAndroidTest</code>. Yes, I know). Why would you want to do this in the first place? Like everything cool and new in Android land, <a href=\"https://d.android.com/jetpack/compose\">Compose</a>! Specifically, <a href=\"https://github.com/jetbrains/compose-jb\">compose-jb</a>, JetBrains’ redistribution of Jetpack Compose optimised for Kotlin Multiplatform.</p>\n<p>I’ve <a href=\"https://github.com/cashapp/paparazzi/pull/450\">sent a PR</a> to Paparazzi that will resolve this issue, and in the mean time we can workaround this limitation.</p>\n<h2 id=\"setting-things-up\">Setting things up</h2>\n<p>To begin, we’ll need a new Gradle module for our Paparazzi tests. Since Paparazzi doesn’t understand Kotlin Multiplatform yet, we’re gonna hide that aspect of our project and present it a pure Android library project. Set up the module like so:</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame has-title\"><figcaption class=\"header\"><span class=\"title\">paparazzi-tests/build.gradle.kts</span></figcaption><pre data-language=\"kotlin\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">plugins</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">id</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"com.android.library\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">id</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"app.cash.paparazzi\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">android</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">buildFeatures</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> { compose </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">true</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"plugins {  id(&quot;com.android.library&quot;)  id(&quot;app.cash.paparazzi&quot;)}android {  buildFeatures { compose = true }}\"><div></div></button></div></figure></div>\n<p>Now, add dependencies in this module to the modules that contain the composables you’d like to test. As you might have guessed, this approach currently limits you to only being able to test public composables. However, if you’re trying to test the UI exposed by a “common” module like I am, that might not be such a big deal.</p>\n<div class=\"expressive-code\"><figure class=\"frame has-title\"><figcaption class=\"header\"><span class=\"title\">paparazzi-tests/build.gradle.kts</span></figcaption><pre data-language=\"kotlin\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">dependencies</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">testImplementation</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(projects.common)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"dependencies {  testImplementation(projects.common)}\"><div></div></button></div></figure></div>\n<p>And that’s pretty much it! You can now be off to the races and start writing your tests:</p>\n<div class=\"expressive-code\"><figure class=\"frame has-title\"><figcaption class=\"header\"><span class=\"title\">paparazzi-tests/src/test/kotlin/UserProfileTest.kt</span></figcaption><pre data-language=\"kotlin\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">class</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">UserProfileTest</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">@get</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">:</span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">Rule</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">val</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">paparazzi</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> = </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">Paparazzi</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">@Test</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">fun</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">light_mode</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">() {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">paparazzi.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">snapshot</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">MaterialTheme</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(colorScheme </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> LightThemeColors) { </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">UserProfile</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">() }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">@Test</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">fun</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">dark_mode</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">() {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">paparazzi.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">snapshot</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">MaterialTheme</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(colorScheme </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> DarkThemeColors) { </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">UserProfile</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">() }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"class UserProfileTest {  @get:Rule val paparazzi = Paparazzi()  @Test  fun light_mode() {    paparazzi.snapshot {      MaterialTheme(colorScheme = LightThemeColors) { UserProfile() }    }  }  @Test  fun dark_mode() {    paparazzi.snapshot {      MaterialTheme(colorScheme = DarkThemeColors) { UserProfile() }    }  }}\"><div></div></button></div></figure></div>\n<p>Consult the <a href=\"https://cashapp.github.io/paparazzi\">Paparazzi documentation</a> for the Gradle tasks reference and customization options.</p>\n<h2 id=\"recipes\">Recipes</h2>\n<h3 id=\"disable-release-build-type-for-test-module\">Disable release build type for test module</h3>\n<p>If you use <code>./gradlew check</code> in your CI, our new module will be tested in both release and debug build types. This is fairly redundant, so you can disable the release build type altogether:</p>\n<div class=\"expressive-code\"><figure class=\"frame has-title\"><figcaption class=\"header\"><span class=\"title\">paparazzi-tests/build.gradle.kts</span></figcaption><pre data-language=\"kotlin\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">androidComponents</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">beforeVariants</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> { variant </span><span style=\"--0:#7B34D9;--1:#FE8929\">-&gt;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">variant.enable </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> variant.buildType </span><span style=\"--0:#116B70;--1:#8EC07C\">==</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"debug\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"androidComponents {  beforeVariants { variant ->    variant.enable = variant.buildType == &quot;debug&quot;  }}\"><div></div></button></div></figure></div>\n<h3 id=\"running-with-jdk-12\">Running with JDK 12+</h3>\n<p>You will run into <a href=\"https://github.com/cashapp/paparazzi/issues/409\">this issue</a> if you use JDK 12 or above to run Paparazzi-backed tests. I’ve <a href=\"https://github.com/cashapp/paparazzi/pull/474\">started working</a> on a fix for it upstream, in the mean time it can be worked around by forcing the test tasks to run with JDK 11.</p>\n<div class=\"expressive-code\"><figure class=\"frame has-title\"><figcaption class=\"header\"><span class=\"title\">paparazzi-tests/build.gradle.kts</span></figcaption><pre data-language=\"kotlin\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">tasks.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">withType</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">&lt;</span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">Test</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">&gt;().</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">configureEach</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">javaLauncher.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">set</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(javaToolchains.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">launcherFor</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">languageVersion.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">set</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(JavaLanguageVersion.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">of</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#A54107;--1:#D995A8\">11</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">))</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">})</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"tasks.withType<Test>().configureEach {  javaLauncher.set(javaToolchains.launcherFor {    languageVersion.set(JavaLanguageVersion.of(11))  })}\"><div></div></button></div></figure></div>\n<h3 id=\"testing-with-multiple-themes-easily\">Testing with multiple themes easily</h3>\n<p>Using an enum and Google’s <a href=\"https://github.com/google/TestParameterInjector\">TestParameterInjector</a> you can write a single test and have it run against all your themes.</p>\n<div class=\"expressive-code\"><figure class=\"frame has-title\"><figcaption class=\"header\"><span class=\"title\">paparazzi-tests/src/test/kotlin/Theme.kt</span></figcaption><pre data-language=\"kotlin\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">import</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#FABD2F\">androidx.compose.material3.ColorScheme</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FE8929\">enum</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">class</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">Theme</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">val</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> colors: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">ColorScheme</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">Light</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(LightThemeColors),</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">Dark</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(DarkThemeColors),</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"import androidx.compose.material3.ColorSchemeenum class Theme(val colors: ColorScheme) {  Light(LightThemeColors),  Dark(DarkThemeColors),}\"><div></div></button></div></figure></div>\n<div class=\"expressive-code\"><figure class=\"frame has-title\"><figcaption class=\"header\"><span class=\"title\">paparazzi-tests/src/test/kotlin/UserProfileTest.kt</span></figcaption><pre data-language=\"kotlin\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">@RunWith</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(TestParameterInjector::</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">class</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">class</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">UserProfileTest</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">@get</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">:</span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">Rule</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">val</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">paparazzi</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> = </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">Paparazzi</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">@Test</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">fun</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">verify</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">@TestParameter</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> theme: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">Theme</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">paparazzi.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">snapshot</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(name </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> theme.name) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">MaterialTheme</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(colorScheme </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> theme.colors) { </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">UserProfile</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">() }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"@RunWith(TestParameterInjector::class)class UserProfileTest {  @get:Rule val paparazzi = Paparazzi()  @Test  fun verify(@TestParameter theme: Theme) {    paparazzi.snapshot(name = theme.name) {      MaterialTheme(colorScheme = theme.colors) { UserProfile() }    }  }}\"><div></div></button></div></figure></div>"
    },
    {
      "id": "https://msfjarvis.dev/posts/converting-gradle-convention-plugins-to-binary-plugins/",
      "url": "https://msfjarvis.dev/posts/converting-gradle-convention-plugins-to-binary-plugins/",
      "title": "Converting Gradle convention plugins to binary plugins",
      "date_published": "2022-04-17T06:30:00.000Z",
      "summary": "Gradle's convention plugins are a fantastic way to share common build configuration, why not take them a step further?",
      "content_html": "<h3 id=\"introduction\">Introduction</h3>\n<p>Gradle’s <a href=\"https://docs.gradle.org/current/samples/sample_convention_plugins.html\">convention plugins</a> are a powerful feature that allow creating simple, reusable Gradle plugins that can be used across your multi-module projects to ensure all modules of a certain type are configured the same way. As an example, if you want to enforce that none of your Android library projects contain a <code>BuildConfig</code> class then the convention plugin for it could look something like this:</p>\n<blockquote>\n<p><code>com.example.android-library.gradle.kts</code></p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"groovy\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">plugins {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">id</span><span style=\"--0:#5D5F6E;--1:#B4A694\">(</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">com.android.library</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">android {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">buildFeatures {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">buildConfig </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--1:#D995A8\">false</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"plugins {  id(&quot;com.android.library&quot;)}android {  buildFeatures {    buildConfig = false  }}\"><div></div></button></div></figure></div>\n</blockquote>\n<p>Then in your modules, you can use this plugin like so:</p>\n<blockquote>\n<p><code>library-module/build.gradle.kts</code></p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"groovy\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">plugins {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">id (</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">com.example.android-library</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"plugins {  id (&quot;com.example.android-library&quot;)}\"><div></div></button></div></figure></div>\n</blockquote>\n<h2 id=\"setting-up-convention-plugins-in-your-project\">Setting up convention plugins in your project</h2>\n<p>Gradle’s official sample linked above mentions <code>buildSrc</code> as the location for your convention plugins. I’m inclined to disagree, <code>buildSrc</code> has historically had issues with IDE support and it’s special status within Gradle’s project handling means any change within <code>buildSrc</code> invalidates caches for your <strong>entire</strong> project resulting in incredible amounts of time lost during incremental builds.</p>\n<p>The solution to all of these problems is <a href=\"https://docs.gradle.org/current/userguide/composite_builds.html\">composite builds</a>, and <a href=\"https://proandroiddev.com/stop-using-gradle-buildsrc-use-composite-builds-instead-3c38ac7a2ab3\">Josef Raska has a fantastic article</a> that thoroughly explains the shortcomings of <code>buildSrc</code> and how composite builds solve them.</p>\n<p>A full explainer on the topic is slightly out of scope for this post, but I can wholeheartedly endorse Jendrik Johannes’ <a href=\"https://github.com/jjohannes/idiomatic-gradle\">idiomatic-gradle</a> repository as an example of setting up the Gradle build of a real-world project while leveraging features introduced in recent versions of Gradle. I highly recommend also checking out their ‘Understanding Gradle’ <a href=\"https://github.com/jjohannes/understanding-gradle#readme\">video series</a>.</p>\n<h2 id=\"why-would-you-want-to-make-binary-plugins-out-of-convention-plugins\">Why would you want to make binary plugins out of convention plugins</h2>\n<p>First, let’s answer this: what is a binary plugin?</p>\n<p>A Gradle plugin that is resolved as a dependency rather than compiled from source is a binary plugin. Binary plugins are cool because the next best thing after a cached compilation task is one that doesn’t exist in the first place.</p>\n<p>For most use cases, convention plugins will need to be updated very infrequently. This means that having each developer execute the plugin build as part of their development process is needlessly wasteful, and we can instead just distribute them as maven dependencies.</p>\n<p>This also makes it significantly easier to share convention plugins between projects without resorting to historically painful solutions like Git submodules or just straight up copy-pasting.</p>\n<h2 id=\"publishing-your-convention-plugins\">Publishing your convention plugins</h2>\n<p>To their credit, Gradle supports this ability very well and you can actually publish all plugins within a build/project with minimal configuration. The changes required to publish <a href=\"https://msfjarvis.dev/aps\">Android Password Store</a>’s convention plugins for Android are:</p>\n<blockquote>\n<p><code>build-logic/android-plugins/build.gradle.kts</code></p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line highlight del\"><div class=\"code\"><span style=\"--0:#484b64;--1:#EBDBB2\">plugins { `kotlin-dsl` }</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66;--1:#EBDBB2\">plugins {</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">`kotlin-dsl`</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">id(\"maven-publish\")</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\">\n</div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66;--1:#EBDBB2\">group = \"com.github.android-password-store\"</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\">\n</div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66;--1:#EBDBB2\">version = \"1.0.0\"</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"plugins { `kotlin-dsl` }plugins {  `kotlin-dsl`  id(&quot;maven-publish&quot;)}group = &quot;com.github.android-password-store&quot;version = &quot;1.0.0&quot;\"><div></div></button></div></figure></div>\n</blockquote>\n<p>After that you can run <code>gradle -p build-logic publishToMavenLocal</code> and it will Just Work:tm:. You can configure additional publishing repositories in similar fashion to how you’d do it for a library project.</p>\n<p>If like me you need to publish these to <a href=\"https://search.maven.org/\">Maven Central</a>, you’ll need slightly more setup since it enforces multiple security and publishing related best practices. Here’s how I use <a href=\"https://github.com/vanniktech/gradle-maven-publish-plugin\">gradle-maven-publish-plugin</a> to configure the same (<code>gradle.properties</code> changes omitted for brevity, the GitHub repository explains what you need):</p>\n<blockquote>\n<p><code>build-logic/settings.gradle.kts</code></p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66;--1:#EBDBB2\">pluginManagement {</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">repositories {</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">mavenCentral()</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">gradlePluginPortal()</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">}</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">plugins {</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">id(\"com.vanniktech.maven.publish.base\") version \"0.19.0\"</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">}</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\">\n</div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"pluginManagement {  repositories {    mavenCentral()    gradlePluginPortal()  }  plugins {    id(&quot;com.vanniktech.maven.publish.base&quot;) version &quot;0.19.0&quot;  }}\"><div></div></button></div></figure></div>\n</blockquote>\n<blockquote>\n<p><code>build-logic/android-plugins/build.gradle.kts</code></p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66;--1:#EBDBB2\">import com.vanniktech.maven.publish.JavadocJar</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66;--1:#EBDBB2\">import com.vanniktech.maven.publish.JavaLibrary</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66;--1:#EBDBB2\">import com.vanniktech.maven.publish.MavenPublishBaseExtension</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66;--1:#EBDBB2\">import com.vanniktech.maven.publish.SonatypeHost</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66;--1:#EBDBB2\">import org.gradle.kotlin.dsl.provideDelegate</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\">\n</div></div><div class=\"ec-line highlight del\"><div class=\"code\"><span style=\"--0:#484b64;--1:#EBDBB2\">plugins { `kotlin-dsl` }</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66;--1:#EBDBB2\">plugins {</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">`kotlin-dsl`</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">id(\"com.vanniktech.maven.publish.base\")</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">id(\"signing\")</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\">\n</div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66;--1:#EBDBB2\">configure&lt;MavenPublishBaseExtension&gt; {</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">group = requireNotNull(project.findProperty(\"GROUP\"))</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">version = requireNotNull(project.findProperty(\"VERSION_NAME\"))</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">publishToMavenCentral(SonatypeHost.DEFAULT)</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">signAllPublications()</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">configure(JavaLibrary(JavadocJar.Empty()))</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">pomFromGradleProperties()</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\">\n</div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\"> </span></span><span style=\"--1:#EBDBB2\">afterEvaluate {</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">signing {</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">val signingKey: String? by project</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">val signingPassword: String? by project</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">useInMemoryPgpKeys(signingKey, signingPassword)</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">}</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\"> </span></span><span style=\"--1:#EBDBB2\">}</span></span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"import com.vanniktech.maven.publish.JavadocJarimport com.vanniktech.maven.publish.JavaLibraryimport com.vanniktech.maven.publish.MavenPublishBaseExtensionimport com.vanniktech.maven.publish.SonatypeHostimport org.gradle.kotlin.dsl.provideDelegateplugins { `kotlin-dsl` }plugins {  `kotlin-dsl`  id(&quot;com.vanniktech.maven.publish.base&quot;)  id(&quot;signing&quot;)}configure<MavenPublishBaseExtension> {  group = requireNotNull(project.findProperty(&quot;GROUP&quot;))  version = requireNotNull(project.findProperty(&quot;VERSION_NAME&quot;))  publishToMavenCentral(SonatypeHost.DEFAULT)  signAllPublications()  configure(JavaLibrary(JavadocJar.Empty()))  pomFromGradleProperties()} afterEvaluate {  signing {    val signingKey: String? by project    val signingPassword: String? by project    useInMemoryPgpKeys(signingKey, signingPassword)  } }\"><div></div></button></div></figure></div>\n</blockquote>\n<p>This will populate your POM files with the properties required by Maven Central and sign all artifacts with PGP.</p>\n<h2 id=\"consuming-your-new-binary-plugins\">Consuming your new binary plugins</h2>\n<p>With your convention plugins converted to shiny new binary plugins, you might be inclined to start using them like so:</p>\n<blockquote>\n<p><code>autofill-parser/build.gradle.kts</code></p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">plugins {</span></div></div><div class=\"ec-line highlight del\"><div class=\"code\"><span style=\"--0:#484b64\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">id(\"com.github.android-password-store.published-android-library\")</span></span></div></div><div class=\"ec-line highlight del\"><div class=\"code\"><span style=\"--0:#484b64\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">id(\"com.github.android-password-store.kotlin-android\")</span></span></div></div><div class=\"ec-line highlight del\"><div class=\"code\"><span style=\"--0:#484b64\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">id(\"com.github.android-password-store.kotlin-library\")</span></span></div></div><div class=\"ec-line highlight del\"><div class=\"code\"><span style=\"--0:#484b64\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">id(\"com.github.android-password-store.psl-plugin\")</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">id(\"com.github.android-password-store.published-android-library\") version \"1.0.0\"</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">id(\"com.github.android-password-store.kotlin-android\") version \"1.0.0\"</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">id(\"com.github.android-password-store.kotlin-library\") version \"1.0.0\"</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">id(\"com.github.android-password-store.psl-plugin\") version \"1.0.0\"</span></span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"plugins {  id(&quot;com.github.android-password-store.published-android-library&quot;)  id(&quot;com.github.android-password-store.kotlin-android&quot;)  id(&quot;com.github.android-password-store.kotlin-library&quot;)  id(&quot;com.github.android-password-store.psl-plugin&quot;)  id(&quot;com.github.android-password-store.published-android-library&quot;) version &quot;1.0.0&quot;  id(&quot;com.github.android-password-store.kotlin-android&quot;) version &quot;1.0.0&quot;  id(&quot;com.github.android-password-store.kotlin-library&quot;) version &quot;1.0.0&quot;  id(&quot;com.github.android-password-store.psl-plugin&quot;) version &quot;1.0.0&quot;}\"><div></div></button></div></figure></div>\n</blockquote>\n<p>However, this fails because <code>kotlin-android</code> and <code>kotlin-library</code> plugins resolve to the same binary JAR that encompasses all plugins from the <code>build-logic/kotlin-plugins</code> module and results in a classpath conflict. To better understand how this resolution works, check out the docs on <a href=\"https://docs.gradle.org/current/userguide/plugins.html#sec:plugin_markers\">plugin markers</a>.</p>\n<p>The way to resolve this problem is to define the plugin versions in your <code>settings.gradle.kts</code> file, where these classpath conflicts will be resolved automatically by Gradle:</p>\n<blockquote>\n<p><code>settings.gradle.kts</code></p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">@@</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> -14,6 +14,25 </span><span style=\"--0:#5D5F6E;--1:#B4A694\">@@</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> pluginManagement {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">     </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">mavenCentral()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">     </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">gradlePluginPortal()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">+</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">  plugins {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">+</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">    id(\"com.github.android-password-store.kotlin-android\") version \"1.0.0\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">+</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">    id(\"com.github.android-password-store.kotlin-library\") version \"1.0.0\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">+</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">    id(\"com.github.android-password-store.psl-plugin\") version \"1.0.0\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">+</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">    id(\"com.github.android-password-store.published-android-library\") version \"1.0.0\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">+</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">  }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"@@ -14,6 +14,25 @@ pluginManagement {     mavenCentral()     gradlePluginPortal()   }+  plugins {+    id(&quot;com.github.android-password-store.kotlin-android&quot;) version &quot;1.0.0&quot;+    id(&quot;com.github.android-password-store.kotlin-library&quot;) version &quot;1.0.0&quot;+    id(&quot;com.github.android-password-store.psl-plugin&quot;) version &quot;1.0.0&quot;+    id(&quot;com.github.android-password-store.published-android-library&quot;) version &quot;1.0.0&quot;+  } }\"><div></div></button></div></figure></div>\n</blockquote>\n<p>And you’re off to the races!</p>\n<h2 id=\"closing-notes\">Closing notes</h2>\n<p>This post was motivated by my goal of sharing a common set of Gradle configurations across my projects such as <a href=\"https://msfjarvis.dev/aps\">Android Password Store</a> and <a href=\"https://msfjarvis.dev/g/compose-lobsters\">Claw</a>, which maintain a nearly identical set of convention plugins shared between the projects that I manually copy-paste back and forth. I’ve extracted the <code>build-logic</code> subproject of APS to a separate <a href=\"https://msfjarvis.dev/g/aps-build-logic\">aps-build-logic</a> repository, set it up for standalone development and configured publishing support. My goal is to supplement this with a continuous deployment workflow where an automatic version bump + release happens after each commit to the main, after which I can migrate my projects to it.</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/backing-up-your-content-from-google-photos/",
      "url": "https://msfjarvis.dev/posts/backing-up-your-content-from-google-photos/",
      "title": "Backing up your content from Google Photos",
      "date_published": "2022-04-04T06:30:00.000Z",
      "summary": "Putting your media into Google Photos is easy, taking it out, not as much.",
      "content_html": "<p>Google Photos has established itself as one of the most popular photo storage services, and like a typical Google service, it makes it impressively difficult to get your data back out of it :D</p>\n<p>There are many good reasons why you’d want to archive your pictures outside of Google Photos, having an extra backup never hurts, maybe you want an offline copy for reasons, or you just want to get your stuff out so you can switch away from Google Photos entirely.</p>\n<h3 id=\"how-to-archive-your-images-from-google-photos\">How to archive your images from Google Photos</h3>\n<ol>\n<li>\n<p>You can use <a href=\"https://takeout.google.com/\">Takeout</a>, except it <strong>always</strong> strips the EXIF metadata of your images, and often generates incomplete archives. Losing EXIF metadata is a deal-breaker, because you can no longer organize images automatically based on properties like date, location, and camera type.</p>\n</li>\n<li>\n<p>You can download directly from <a href=\"https://photos.google.com/\">photos.google.com</a> which preserves metadata, but is embarassingly manual and basically impossible to use if you’re trying archive a few years of history.</p>\n</li>\n</ol>\n<p>So, what’s the solution?</p>\n<h3 id=\"gphotos-cdp\">gphotos-cdp</h3>\n<p><a href=\"https://github.com/perkeep/gphotos-cdp\">gphotos-cdp</a> is a tool that uses the nearly-perfect method number 2 and makes it automated. It does so by using the <a href=\"https://chromedevtools.github.io/devtools-protocol/\">Chrome DevTools Protocol</a> to drive an instance of the Google Chrome browser, and emulates all the manual actions you’d take as a human to ensure you get copies of your pictures with all the EXIF metadata retained.</p>\n<h3 id=\"setting-up-gphotos-cdp\">Setting up gphotos-cdp</h3>\n<blockquote>\n<p>Disclaimer: I’ve only tested this on Linux. This <em>should</em> be doable on other platforms, but it’s not relevant to my needs so I will not be investigating that.</p>\n</blockquote>\n<p>Ideally you’d want to run this tool on a schedule on a NAS or a server to keep archiving images automatically as they get added to your Google Photos. I personally run this inside a hosted VM on a daily schedule.</p>\n<p>For gphotos-cdp to run in a non-interactive manner, it requires your browser data directory with your Google login cookies. You can easily create this with the following command:</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame is-terminal\"><figcaption class=\"header\"><span class=\"title\"></span><span class=\"sr-only\">Terminal window</span></figcaption><pre data-language=\"bash\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">google-chrome</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#D995A8\">\\</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#2C6E1E;--1:#D995A8\">--user-data-dir=gphotos-cdp</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#D995A8\">\\</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#2C6E1E;--1:#D995A8\">--no-first-run</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span><span style=\"--0:#8D477A;--1:#D995A8\">\\</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#2C6E1E;--1:#D995A8\">--password-store=basic</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#D995A8\">\\</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#2C6E1E;--1:#D995A8\">--use-mock-keychain</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#D995A8\">\\</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">https://photos.google.com/</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"google-chrome \\  --user-data-dir=gphotos-cdp \\  --no-first-run  \\  --password-store=basic \\  --use-mock-keychain \\  https://photos.google.com/\"><div></div></button></div></figure></div>\n<p>This will launch Google Chrome with a brand new profile. Login to Photos, and then close the browser. Optionally, re-run the command to ensure that you do not need to login again.</p>\n<blockquote>\n<p>The flags passed to google-chrome are extracted from the default set of parameters used by gphotos-cdp. I wish I could explain why each flag is necessary, but all I know is that it does the trick. I got them from <a href=\"https://github.com/perkeep/gphotos-cdp/issues/1#issuecomment-567378082\">this GitHub comment</a> on the issue tracker for gphotos-cdp.</p>\n</blockquote>\n<p>Once done, you’ll have a <code>gphotos-cdp</code> directory that you’ll need to move to the <code>/tmp</code> directory of whichever machine you wish to run gphotos-cdp on.</p>\n<p>gphotos-cdp is written in <a href=\"https://go.dev\">Golang</a> so you’ll need to install it first. Once done, run the following command to install the latest version of gphotos-cdp</p>\n<div class=\"expressive-code\"><figure class=\"frame is-terminal\"><figcaption class=\"header\"><span class=\"title\"></span><span class=\"sr-only\">Terminal window</span></figcaption><pre data-language=\"bash\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">go</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">install</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">github.com/perkeep/gphotos-cdp@latest</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"go install github.com/perkeep/gphotos-cdp@latest\"><div></div></button></div></figure></div>\n<p>Then you can go ahead and start using gphotos-cdp, as given below</p>\n<div class=\"expressive-code\"><figure class=\"frame is-terminal\"><figcaption class=\"header\"><span class=\"title\"></span><span class=\"sr-only\">Terminal window</span></figcaption><pre data-language=\"bash\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#116B70;--1:#8EC07C\">~</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">/go/bin/gphotos-cdp </span><span style=\"--0:#8D477A;--1:#D995A8\">\\ </span><span style=\"--0:#4C4F69;--1:#EBDBB2\"># go install puts things in </span><span style=\"--0:#116B70;--1:#8EC07C\">~</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">/go/bin by default</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">-v</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#D995A8\">\\ </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">#</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Enable</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">verbose</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">logging</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">-dev</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#D995A8\">\\ </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">#</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Enable</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">dev</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">mode</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">which</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">always</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">uses</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">/tmp/gphotos-cdp</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">as</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">the</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">profile</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">directory</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">-headless</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#D995A8\">\\ </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">#</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Run</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Chrome</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">in</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">headless</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">mode</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">so</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">it</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">works</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">on</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">servers</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">and</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">such</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">-dldir</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">~/photos</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Download everything to ~/photos</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"~/go/bin/gphotos-cdp \\ # go install puts things in ~/go/bin by default  -v \\ # Enable verbose logging  -dev \\ # Enable dev mode which always uses /tmp/gphotos-cdp as the profile directory  -headless \\ # Run Chrome in headless mode so it works on servers and such  -dldir ~/photos # Download everything to ~/photos\"><div></div></button></div></figure></div>\n<p>The automation techniques used are not completely reliable and can often fail. You’ll want to implement some kind of retry-on-failure logic to ensure this is run a few times every day.</p>\n<h3 id=\"monitoring\">Monitoring</h3>\n<p>With anything built on such a brittle foundation, it’s useful to be able to constantly monitor that things are working as they should.</p>\n<p>Using <a href=\"https://healthchecks.io\">healthchecks.io</a> you can easily set up alerts that notify you of failures running the tool or unintentional gaps in the schedule you run the gphotos-cdp on. I use my <a href=\"https://msfjarvis.dev/g/healthchecks-rs\">healthchecks-monitor</a> CLI in a <a href=\"https://man7.org/linux/man-pages/man5/crontab.5.html\">cron</a> job to run gphotos-cdp every day, and healthchecks.io notifies me via Telegram when it fails. The script running in cron looks like this</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"bash\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#8D477A;--0fs:italic;--1:#B1A69C;--1fs:italic\">#!/usr/bin/env bash</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#91AFA3\">HEALTHCHECKS_CHECK_ID</span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">&lt;UUID</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">for</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">check</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">as</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">given</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">on</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">healthchecks&gt;</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#D995A8\">\\</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">HEALTHCHECKS_USERAGENT=crontab </span><span style=\"--0:#8D477A;--1:#D995A8\">\\</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">~/bin/healthchecks-monitor </span><span style=\"--0:#2C6E1E;--1:#D995A8\">--retries</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">3</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#D995A8\">\\ </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">#</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Try</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">running</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">the</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">command</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">thrice</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">before</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">giving</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">up</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">--timer</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#D995A8\">\\ </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">#</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Start</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">off</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">a</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">server-side</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">timer</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">on</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">healthchecks</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">--logs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#8D477A;--1:#D995A8\">\\ </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">#</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Record</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">execution</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">logs</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">on</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">healthchecks</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">in</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">case</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">of</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">failure,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">to</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">help</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">with</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">debugging</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">--exec</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">~/go/bin/gphotos-cdp -v -dev -headless -dldir ~/photos</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"#!/usr/bin/env bashHEALTHCHECKS_CHECK_ID=<UUID for check as given on healthchecks> \\HEALTHCHECKS_USERAGENT=crontab \\~/bin/healthchecks-monitor --retries 3 \\ # Try running the command thrice before giving up  --timer \\ # Start off a server-side timer on healthchecks  --logs \\ # Record execution logs on healthchecks in case of failure, to help with debugging  --exec &quot;~/go/bin/gphotos-cdp -v -dev -headless -dldir ~/photos&quot;\"><div></div></button></div></figure></div>\n<h3 id=\"conclusion\">Conclusion</h3>\n<p>As evident, it’s not an easy task to automatically archive your pictures from Google Photos. The setup is tedious and prone to breakage when any authentication related change happens, such as you accidentally logging out the “device” being used by gphotos-cdp or changing your password, in which case you will need to create the <code>gphotos-cdp</code> directory with Chrome again.</p>\n<p>Also, the technique in this post could easily stop working at any time if Google chooses to break it. That being said, gphotos-cdp was last updated in 2020 and still continues to function as-is so there is some degree of hope that it can be used for quite a bit more.</p>\n<p>Hopefully this setup causes you minimal grief and allows you to back up your precious memories without relying only on Google :)</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/migrating-aps-to-material-you/",
      "url": "https://msfjarvis.dev/posts/migrating-aps-to-material-you/",
      "title": "Migrating APS to Material You",
      "date_published": "2021-11-06T06:30:00.000Z",
      "summary": "I recently migrated Password Store to Material You, Google's latest iteration of Material Design. Here's how it went.",
      "content_html": "<p>With much fanfare, Google released the next iteration of Material Design: <strong>Material You</strong>. It’s received mixed reviews, but I found it extremely pleasant to use and the homogeneity of Google apps following the platform colors felt great. That’s what prompted me to update APS to Material You and join in :)</p>\n<p>As expected, the library ecosystem (specifically the <a href=\"https://github.com/material-components/material-components-android\">material-components-android</a> library) took a while to support Material You but with the <a href=\"https://github.com/material-components/material-components-android/releases/tag/1.5.0-alpha05\">1.5.0-alpha05</a> release of the MDC Android library, things are finally at a place where migration to Material You (henceforth referred to as M3) is viable for simpler apps like APS.</p>\n<p>APS has had some design work done to it but for the most part remains the culmination of ad-hoc choices (often bad) over a period of several years. With this migration I sought to change that and make things a bit more cohesive as well as give the app some much needed <em>oomph</em>.</p>\n<h2 id=\"getting-the-basics-in\">Getting the basics in</h2>\n<p>I began with scouring through the resources in the conveniently isolated <a href=\"https://m3.material.io\">M3 website</a>, where the Material Design team has helpfully created a lot of great tools and content to help developers and designers through the process. There’s the “<a href=\"https://material.io/blog/migrating-material-3\">Migration to Material 3</a>” blog post, and the <a href=\"https://material.io/blog/material-theme-builder\">Material Theme Builder</a> to generate palettes/styles/themes for apps, for both Jetpack Compose and Android’s View-based system. These were extremely helpful in getting a headstart on the whole process. It’s all documented in the commit history of the <a href=\"https://github.com/android-password-store/Android-Password-Store/pull/1532/commits\">migration PR</a> but I figure some additional context can’t hurt.</p>\n<p>Once I had the themes in, I decided to take the opportunity to also introduce a custom typeface. The app has been using Roboto since forever and it felt like it was time to spice things up. I decided to go with <a href=\"https://fonts.google.com/specimen/Manrope#about\">Manrope</a> since it is a font I’ve previously used and found to be excellent for visual appeal and accessibility. I’m still not a 100% confident in my choice, so if people have better options in mind I’d love to know down in the comments.</p>\n<p>Once the new font face was in, I opted to enable dynamic colors. Admittedly not the right choice, since I should’ve validated the “default” palette first but that’s what I did ¯\\<em>(ツ)</em>/¯.</p>\n<h2 id=\"bugfixes-and-improvements️\">Bugfixes and improvements™️</h2>\n<p>Once the M3 themes were all prepped, it was time to actually start migrating.</p>\n<p>I switched our activities to use the M3 themes, and immediately started noticing bugs from non-idiomatic and straight up incorrect theming we’ve been lugging around for the past couple years.</p>\n<p>First step was to update our iconography, which was using inconsistent tints throughout. I updated all of them to use <code>?attr/colorControlNormal</code> which made them blend in correctly with the rest of the updated UI.</p>\n<p>Earlier this year we had migrated a selection UI in one of our screens to use <a href=\"https://material.io/components/chips\">Chips</a>, but we never got the theming right so it always looked kind of wrong. With M3, we were able to revert back to <code>MaterialButtonToggleGroup</code> without regressing on the <a href=\"https://github.com/android-password-store/Android-Password-Store/issues/1261\">accessibility issue</a> which made us do it in the first place.</p>\n<p>There were a lot more smaller changes that were made to address the remaining visual bugs</p>\n<ul>\n<li>Our onboarding flow was using an incorrect interpretation of <code>?attr/colorPrimary</code> for theming, and was migrated to use <code>?android:attr/colorBackground</code></li>\n<li>A lot of screens were using hard-coded colors, which were migrated to theme attributes</li>\n<li>Many screens used hard-coded styles for buttons and text fields, and were also migrated to theme attributes</li>\n<li>Multiple layouts also referenced typography styles directly and were migrated to the corresponding M3 attributes, based on the mapping table in the “<a href=\"https://material.io/blog/migrating-material-3\">Migration to Material 3</a>” article.</li>\n<li>System bars and Toolbar had to be given explicit styles and colors to match the “flat” aesthetic from our M2 designs.</li>\n</ul>\n<h2 id=\"the-final-stretch\">The final stretch</h2>\n<p>With the visual fixes out of the way, I went in and cleaned up the themes and styles. I commonized shared attributes such as fonts and widget styles, created M3 variants of other special-purpose themes we had, and got rid of all the now unused M2 theming. Overall, the PR touched 60+ separate files and generated a final diff of <code>+603,-314</code> lines. The PR can be seen <a href=\"https://msfjarvis.dev/aps/pr/1532\">here</a>.</p>\n<p>We use a third-party library by <a href=\"https://github.com/maxr1998\">Max Rumpf</a> called <a href=\"https://github.com/Maxr1998/ModernAndroidPreferences\">ModernAndroidPreferences</a> for our settings UI, and it hard-coded the use of AppCompat dialogs. Max was extremely helpful and made that customisable for us over the weekend which allowed us to use the appropriate Material You dialogs consistently. Huge thanks to Max, and check out his library! &lt;3</p>\n<h2 id=\"screenshots\">Screenshots!</h2>\n<h3 id=\"before\">Before</h3>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/aps_m2_gallery.BLIsUUkn.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/aps_m2_gallery.BLIsUUkn.webp\" data-pswp-width=\"2160\" data-pswp-height=\"1170\" aria-label=\"Screenshot gallery of a few APS screens before the Material 3 migration\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/aps_m2_gallery.BLIsUUkn.webp\" alt=\"Screenshot gallery of a few APS screens before the Material 3 migration\" loading=\"lazy\" decoding=\"async\" width=\"2160\" height=\"1170\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a></figure>\n<h3 id=\"after\">After</h3>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/aps_m3_gallery.PtBdsfGm.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/aps_m3_gallery.PtBdsfGm.webp\" data-pswp-width=\"2160\" data-pswp-height=\"1170\" aria-label=\"Screenshot gallery of a few APS screens after the Material 3 migration\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/aps_m3_gallery.PtBdsfGm.webp\" alt=\"Screenshot gallery of a few APS screens after the Material 3 migration\" loading=\"lazy\" decoding=\"async\" width=\"2160\" height=\"1170\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a></figure>\n<h2 id=\"closing-notes\">Closing notes</h2>\n<p>APS is a very low-effort app when it comes to UI work. We do not have a custom design system, everything follows Material to a T, and we try to stay in that lane. Our migration took me around 9 hours of work over two days, most of which was really spent on menial work such as manually checking all layouts for hard-coded styles and replacing them with attributes. This isn’t representative of what this process would look like for any project which rolls its own design system on top of Material, since they have a lot more to do before they can even <em>begin</em> the migration of their screens.</p>\n<p>I’d like to thank the Material Design team once more for the fabulous work they have done both in creating Material You as well as the technical documentation around it. <a href=\"https://material.io/blog/material-theme-builder\">Material Theme Builder</a> was an extremely crucial tool for me that set the tone of the whole process, and I would have certainly repeated the same mistakes I did with Material 2 if it wasn’t for the tooling and guidance from the team.</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/building-static-rust-binaries-for-linux/",
      "url": "https://msfjarvis.dev/posts/building-static-rust-binaries-for-linux/",
      "title": "Building static Rust binaries for Linux",
      "date_published": "2021-10-17T06:30:00.000Z",
      "summary": "Some tips on building static binaries of Rust projects targeting Linux",
      "content_html": "<p>Rust has supported producing statically linked binaries since <a href=\"https://github.com/rust-lang/rfcs/pull/1721\">RFC #1721</a> which proposed the <code>target-feature=+crt-static</code> flag to statically link the platform C library into the final binary. This was initially only supported for Windows MSVC and the MUSL C library. While MUSL works for <em>most</em> people, it\nhas many problems by virtue of being a work-in-progress such as <a href=\"https://www.reddit.com/r/rust/comments/a6pna3/why_rust_uses_glibc_and_not_musl_by_default_for/ebzpzld/\">unpredictable performance</a> and many unimplemented features which programs tend to assume are present due to glibc being ubiquitous. In lieu of these concerns, support was added to Rust in 2019 to be able to <a href=\"https://github.com/rust-lang/rust/issues/65447\">statically link against glibc</a>.</p>\n<p>Unfortunately, if you try to directly use it with <code>RUSTFLAGS='-C target-feature=+crt-static' cargo build</code> there is a good chance you’ll run into an error similar to this:</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plaintext\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">cannot produce proc-macro for `async-trait v0.1.51` as the target `x86_64-unknown-linux-gnu` does not support these crate types</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"cannot produce proc-macro for `async-trait v0.1.51` as the target `x86_64-unknown-linux-gnu` does not support these crate types\"><div></div></button></div></figure></div>\n<p>This is a bit of a head scratcher, because the target (your host machine) <em>definitely</em> supports proc-macro crates. Turns out, even Rust contributors <a href=\"https://github.com/rust-lang/rust/issues/78210\">were confused by this</a>. The “fix” for this is apparently to pass in the <code>--target</code> explicitly. The reason behind this seems to be a bug with cargo, where the <code>RUSTFLAGS</code> are applied to the target platform only when <code>--target</code> is explicitly provided. Without it, <code>RUSTFLAGS</code> values are set for the host only which results in the errors we see. More details are available <a href=\"https://github.com/rust-lang/rust/issues/78210#issuecomment-714776007\">Rust issue #78210</a></p>\n<p>Therefore, the correct way to build a statically linked glibc executable for an x86_64 machine is this:</p>\n<div class=\"expressive-code\"><figure class=\"frame is-terminal\"><figcaption class=\"header\"><span class=\"title\"></span><span class=\"sr-only\">Terminal window</span></figcaption><pre data-language=\"shell\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#91AFA3\">RUSTFLAGS</span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#2C6E1E;--1:#B4A694\">'</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">-C target-feature=+crt-static</span><span style=\"--0:#2C6E1E;--1:#B4A694\">'</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">cargo</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">build</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#D995A8\">--release</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#D995A8\">--target</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">x86_64-unknown-linux-gnu</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"RUSTFLAGS='-C target-feature=+crt-static' cargo build --release --target x86_64-unknown-linux-gnu\"><div></div></button></div></figure></div>\n<h2 id=\"other-potential-problems\">Other potential problems</h2>\n<p>You may be unable to statically link your binary even after all this, due to dependencies that <em>mandate</em> dynamic linking. In some cases this is avoidable, such as using <a href=\"https://crates.io/crates/rustls\">rustls</a> in place of OpenSSL for cryptography, and <a href=\"https://crates.io/crates/reqwest\">reqwest</a> or <a href=\"https://crates.io/crates/ureq\">ureq</a> in place of bindings to cURL for HTTP, not so much in others. Thanks to the convention of native-linking crates using the <code>-sys</code> suffix in their name it is fairly simple to find if your build has dependencies that dynamically link to libraries. Using <code>cargo</code>’s native <code>tree</code> subcommand and <code>grep</code>ing (or <a href=\"https://crates.io/crates/ripgrep\">ripgrep</a>ing for me), you can locate native dependencies. Running <code>cargo tree | rg -- -sys</code> against <a href=\"https://msfjarvis.dev/g/androidx-release-watcher\">androidx-release-watcher</a>’s <code>v4.1.0</code> release gives us this:</p>\n<div class=\"expressive-code\"><figure class=\"frame is-terminal\"><figcaption class=\"header\"><span class=\"title\"></span><span class=\"sr-only\">Terminal window</span></figcaption><pre data-language=\"bash\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">$</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">cargo</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">tree</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">|</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">rg</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#D995A8\">--</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#D995A8\">-sys</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">├──</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">curl-sys</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">v0.4.45+curl-7.78.0</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">├──</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">libnghttp2-sys</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">v0.1.6+1.43.0</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">├──</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">libz-sys</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">v1.1.3</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">└──</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">openssl-sys</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">v0.9.66</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">├──</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">openssl-sys</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">v0.9.66</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (*)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">├──</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">curl-sys</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">v0.4.45+curl-7.78.0</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (*)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">   </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">└──</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">web-sys</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">v0.3.53</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">│</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">       </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">├──</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">js-sys</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">v0.3.53</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"$ cargo tree | rg -- -sys│   │   │   │   ├── curl-sys v0.4.45+curl-7.78.0│   │   │   │   │   ├── libnghttp2-sys v0.1.6+1.43.0│   │   │   │   │   ├── libz-sys v1.1.3│   │   │   │   │   └── openssl-sys v0.9.66│   │   │   │   ├── openssl-sys v0.9.66 (*)│   │   │   ├── curl-sys v0.4.45+curl-7.78.0 (*)│   └── web-sys v0.3.53│       ├── js-sys v0.3.53\"><div></div></button></div></figure></div>\n<p>This indicates curl, zlib, openssl, and libnghttp2 as well as a bunch of WASM-related things are being dynamically linked into my executable. To resolve this, I looked at the build features exposed by <a href=\"https://crates.io/crates/surf\">surf</a> and found that it selects the <code>\"curl_client\"</code> feature by default, which can be turned off and replaced with <code>\"h1-client-rustls\"</code> which uses an HTTP client backed by <a href=\"https://crates.io/crates/rustls\">rustls</a> and <a href=\"https://crates.io/crates/async-std\">async-std</a> and no dynamically linked libraries. Enabling <a href=\"https://msfjarvis.dev/g/androidx-release-watcher/b67a212106d8\">this build feature</a> removed all <code>-sys</code> dependencies from <a href=\"https://msfjarvis.dev/g/androidx-release-watcher\">androidx-release-watcher</a>, allowing me to build static executables of it.</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/learning-zig--day-4/",
      "url": "https://msfjarvis.dev/posts/learning-zig--day-4/",
      "title": "Learning Zig - Day 4",
      "date_published": "2021-05-18T06:30:00.000Z",
      "summary": "Brushing up on standards",
      "content_html": "<p>Today I’ll be getting familiar with common patterns in Zig like providing explicit allocators and learn about more of the standard library. This is of course, <a href=\"https://ziglearn.org/chapter-2/\">chapter 2</a> of the ZigLearn.org curriculum.</p>\n<h1 id=\"thoughts️\">Thoughts™️</h1>\n<h2 id=\"allocators\">Allocators</h2>\n<p>Rust’s standard library performs allocations as and when necessary, automatically, and provides a separate <code>no_std</code> mode that eliminates the standard library from the build and only retains the <a href=\"https://doc.rust-lang.org/core/index.html\">libcore</a> which is suitable for use in bare-metal deployments (more on that in <a href=\"https://docs.rust-embedded.org/book/intro/no-std.html\">The Embedded Rust Book</a>). Zig takes an alternative approach, where the convention is that all functions in the the standard library that require allocations take in an explicit <a href=\"https://ziglang.org/documentation/0.7.1/std/#std;mem.Allocator\">Allocator</a> which can either be implemented by the user, or you can pick one of the many options available in the standard library itself.</p>\n<p>I’ve never given an <em>extreme</em> amount of thought to the allocations my programs perform, so I’m rather unopinionated on this. That being said, whenever I start working on the healthchecks library I’m definitely going to follow the ecosystem’s practices and make allocations explicit for consumers.</p>\n<h2 id=\"filesystem\">Filesystem</h2>\n<p>The FS APIs appear to be quite expansive, covering everything that I can think of, and I finally discovered a practical use-case for <code>defer</code>! I’m pretty sure I’ll end up passing the incorrect <a href=\"https://ziglang.org/documentation/0.7.1/std/#std;fs.File.CreateFlags\">CreateFlags</a> on more than one occasion, but at least they’re documented and discoverable, and I have <em>some</em> memory of doing the same with Python back when I still believed in my country’s education system :P</p>\n<h2 id=\"formatting\">Formatting</h2>\n<p>Finally an answer to my “how to format an array” mystery! It’s fascinating that formatting also requires allocations, but then Rust does return an owned <code>String</code> rather than a <code>&amp;str</code> when you format things so that should have been obvious in hindsight.</p>\n<h2 id=\"json\">JSON</h2>\n<p>The native JSON support looks pretty great, though I am left to wonder how much of <a href=\"https://serde.rs/\">serde</a>’s flexibility is available here. Guess we’ll find out soon enough :)</p>\n<h2 id=\"random-numbers-and-crypto\">Random numbers and crypto</h2>\n<p>Great to see native crypto in Zig! I’m not the target audience, but lack of crypto primitives in Rust seems to come up often so it’s a nice plus for Zig.</p>\n<h2 id=\"formatting-specifiers-and-advanced-formatting\">Formatting specifiers and Advanced Formatting</h2>\n<p>Zig’s formatting system seems about as powerful as Rust’s, <em>maybe more</em>, so it’s definitely a plus for me since I’ve ended up debugging a lot of code with <code>println!(\"{:?}\", $field)</code> in Rust 😅</p>\n<h1 id=\"conclusion\">Conclusion</h1>\n<p>I skipped through the parts about HashMaps, Stacks, sorting and iterators since those are fairly straightforward concepts that Zig does not appear to reinvent in any way.</p>\n<p>Overall, I’m liking everything I’m seeing. Very excited to start building things in Zig!</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/learning-zig--day-3/",
      "url": "https://msfjarvis.dev/posts/learning-zig--day-3/",
      "title": "Learning Zig - Day 3",
      "date_published": "2021-05-16T06:30:00.000Z",
      "summary": "Finishing up the basics",
      "content_html": "<p><a href=\"https://msfjarvis.dev/posts/learning-zig--day-2\">Yesterday’s post</a> was a bit shorter than I planned, since I didn’t manage to go through as much of the ZigLearn <a href=\"https://ziglearn.org/chapter-1/\">chapter 1</a> as I thought I would. Today we’ll be wrapping it up.</p>\n<h1 id=\"thoughts️\">Thoughts™️</h1>\n<p>Same as yesterday, this section will be a brain dump of what I think of the things I learn today.</p>\n<h2 id=\"pointers\">Pointers</h2>\n<p>Rust made pointers a very friendly concept thanks to the borrow checker and amazing compiler diagnostics, and Zig seems to follow the same path with keeping them straightforward. I’m not the biggest fan of the <code>variable.*</code> syntax for dereferencing a pointer, since it breaks my existing muscle memory in a major way but I’m sure I’ll get used to it in no time.</p>\n<p>Just like Rust, mutable and immutable pointers are explicitly distinct which is a ✅ in my book.</p>\n<p>There wasn’t a lot of content on ZigLearn about <a href=\"https://ziglearn.org/chapter-1/#many-item-pointers\">many-item pointers</a> so I’m still not sure I understand any of it. That’s probably just me though.</p>\n<p>I’ve known and used Rust’s <code>usize</code> in my programs, but only after reading about <a href=\"https://ziglearn.org/chapter-1/#pointer-sized-integers\">pointer-sized integers</a> on ZigLearn did I actually make the connection that the size of a <code>usize</code> is that of a pointer. 💡</p>\n<h2 id=\"enums\">Enums</h2>\n<p>On a syntactic level, Zig enums are closer to Kotlin than to Rust w.r.t. declaring functions in them, which is very nice.</p>\n<h2 id=\"structs\">Structs</h2>\n<p>The syntax note from enums applies here as well, with an additional nicety about pointers. Specifically, a struct function that accepts a pointer value will automatically dereference the value inside the function body. This only goes one level deep though, so keep that in mind.</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"zig\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">const</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">Rectangle</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">struct</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69\">    </span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">length</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">i32</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69\">    </span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">width</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">i32</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">pub</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">fn</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">swap</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#4C4F69;--1:#91AFA3\">self</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">*</span><span style=\"--0:#4C4F69;--1:#91AFA3\">Rectangle</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) </span><span style=\"--0:#7B34D9;--1:#FC8678\">void</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// No explicit dereferencing needed!</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#7B34D9;--1:#FC8678\">const</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">tmp</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">self</span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">length</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69\">        </span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">self</span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">length</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">self</span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">width</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69\">        </span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">self</span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">width</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">tmp</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"const Rectangle = struct {    length: i32,    width: i32,    pub fn swap(self: *Rectangle) void {        // No explicit dereferencing needed!        const tmp = self.length;        self.length = self.width;        self.width = tmp;    }};\"><div></div></button></div></figure></div>\n<h2 id=\"unions\">Unions</h2>\n<p>I have never worked with <code>union</code>s, but <a href=\"https://ziglang.org/documentation/master/#Tagged-union\">tagged unions</a> gave me awful ideas about matching Kotlin’s <a href=\"https://kotlinlang.org/docs/sealed-classes.html\">sealed classes</a> functionality so I’m looking forward to writing some cursed code :D</p>\n<h2 id=\"integer-rules-and-floats\">Integer rules and Floats</h2>\n<p>Zig’s type coercion syntax is nicer than Rust’s, though the lack of a runtime error concerned me initially. Rust’s <a href=\"https://doc.rust-lang.org/std/convert/trait.TryInto.html\">TryInto</a> trait is explicit about the fact that the conversion is fallible, and thus returns a <a href=\"https://doc.rust-lang.org/std/result/enum.Result.html\">Result</a>. Zig on the other hand attempts to validate these conversions at compile-time. Given this code:</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"zig\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FE8929\">fn</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">returnsNum</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">() </span><span style=\"--0:#7B34D9;--1:#FC8678\">u32</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">return</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">1_00_000</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">test</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"typecast\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69\">    </span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">testing</span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">expect</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#C00E34;--0fs:italic;--1:#FE8929\">@TypeOf</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#C00E34;--0fs:italic;--1:#FE8929\">@as</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">u8</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">returnsNum</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">())) </span><span style=\"--0:#116B70;--1:#8EC07C\">==</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">u8</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"fn returnsNum() u32 {    return 1_00_000;}test &quot;typecast&quot; {    testing.expect(@TypeOf(@as(u8, returnsNum())) == u8);}\"><div></div></button></div></figure></div>\n<p>The build will fail with:</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plaintext\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">./src/main.zig:182:46: error: expected type 'u8', found 'u32'</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">testing.expect(@TypeOf(@as(u8, returnsNum())) == u8);</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">                                             </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">^</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">./src/main.zig:182:46: note: unsigned 8-bit int cannot represent all possible unsigned 32-bit values</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">    </span></span><span style=\"--0:#4c4f69;--1:#ebdbb2\">testing.expect(@TypeOf(@as(u8, returnsNum())) == u8);</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"./src/main.zig:182:46: error: expected type 'u8', found 'u32'    testing.expect(@TypeOf(@as(u8, returnsNum())) == u8);                                             ^./src/main.zig:182:46: note: unsigned 8-bit int cannot represent all possible unsigned 32-bit values    testing.expect(@TypeOf(@as(u8, returnsNum())) == u8);\"><div></div></button></div></figure></div>\n<p>This is good, but I haven’t yet found a way to force the conversion to go through in instances where I can confirm that the incoming u32 definitely fits in a u8.</p>\n<h2 id=\"optionals\">Optionals</h2>\n<p>Zig’s <a href=\"https://ziglang.org/documentation/master/#Optionals\">Optionals</a> are a very good parallel for Rust’s <a href=\"https://doc.rust-lang.org/std/option/enum.Option.html\">Option</a>, though Zig provides a lot more syntactic niceties.</p>\n<p>The fact that you can use a while loop to capture values until they become null is pretty damn sweet.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"zig\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">var</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">numbers_left</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">u32</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">4</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FE8929\">fn</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">eventuallyNullSequence</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">() </span><span style=\"--0:#116B70;--1:#8EC07C\">?</span><span style=\"--0:#7B34D9;--1:#FC8678\">u32</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">if</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (</span><span style=\"--0:#4C4F69;--1:#91AFA3\">numbers_left</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">==</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">0</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) </span><span style=\"--0:#7B34D9;--1:#FC8678\">return</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">null</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69\">    </span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">numbers_left</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">-=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">1</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">return</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">numbers_left</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">test</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"while null capture\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">var</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">sum</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">u32</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">0</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">while</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">eventuallyNullSequence</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()) </span><span style=\"--0:#116B70;--1:#8EC07C\">|</span><span style=\"--0:#4C4F69;--1:#91AFA3\">value</span><span style=\"--0:#116B70;--1:#8EC07C\">|</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69\">        </span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">sum</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">+=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">value</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">expect</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#4C4F69;--1:#91AFA3\">sum</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">==</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">6</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// 3 + 2 + 1</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"var numbers_left: u32 = 4;fn eventuallyNullSequence() ?u32 {    if (numbers_left == 0) return null;    numbers_left -= 1;    return numbers_left;}test &quot;while null capture&quot; {    var sum: u32 = 0;    while (eventuallyNullSequence()) |value| {        sum += value;    }    expect(sum == 6); // 3 + 2 + 1}\"><div></div></button></div></figure></div>\n<h1 id=\"conclusion\">Conclusion</h1>\n<p>Everything following optionals was either uncontroversial or too powerful/low level for my current interests, so I admittedly glossed over some of the gory details.</p>\n<p><a href=\"https://ziglearn.org/chapter-2/\">Chapter 2</a> introduces JSON, which we’ll need for our eventual healthchecks.io library, so I’m looking forward to it! I do have work tomorrow, so we’ll have to see if I can keep up the daily streak :)</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/learning-zig--day-2/",
      "url": "https://msfjarvis.dev/posts/learning-zig--day-2/",
      "title": "Learning Zig - Day 2",
      "date_published": "2021-05-15T06:30:00.000Z",
      "summary": "Onwards in our quest to learn Zig",
      "content_html": "<p>In the <a href=\"https://msfjarvis.dev/posts/first-steps-with-zig\">previous post</a> I documented how I went about setting up my Zig environment, and it’s now time to start learning things.</p>\n<p>My preferred method of learning new languages is rebuilding an existing project in them, like I did when going from <a href=\"https://msfjarvis.dev/g/walls-manager\">Python</a> to <a href=\"https://msfjarvis.dev/g/walls-bot\">Kotlin</a> to <a href=\"https://msfjarvis.dev/g/walls-bot-rs\">Rust</a>. For Zig, I’ve elected to rebuild my <a href=\"https://msfjarvis.dev/g/healthchecks-rs\">healthchecks-rs</a> library. It’s something I use on a day-to-day basis for keeping an eye on my backup jobs, and it would be a great addition to the <a href=\"https://healthchecks.io/docs/resources/\">healthchecks.io ecosystem</a>.</p>\n<h1 id=\"getting-the-basics-down\">Getting the basics down</h1>\n<p>Among the resources enlisted on the Zig <a href=\"https://ziglang.org/learn/getting-started/\">getting started</a> page, I opted to go with <a href=\"https://ziglearn.org/\">ziglearn.org</a> for learning the ropes of the language. It is concise yet detailed, and the chapter-wise breakdown makes for great mental “checkpoints”, much like the <a href=\"https://doc.rust-lang.org/book/\">Rust book</a>.</p>\n<p>For this post I’m going through <a href=\"https://ziglearn.org/chapter-1/\">chapter 1</a>.</p>\n<h1 id=\"thoughts️\">Thoughts™️</h1>\n<p>I’m going to use this section to jot down my thoughts about Zig, broken down by the sections on ZigLearn. I’ll skip the parts that I don’t have anything to say on.</p>\n<h2 id=\"assignment\">Assignment</h2>\n<p>The presence of <code>undefined</code> is <em>very</em> interesting to me. It appears to be functionally identical to Rust’s <a href=\"https://doc.rust-lang.org/std/default/trait.Default.html\">Default trait</a>, as shown in this snippet (had to skip to structs for this since I was so curious about it).</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"zig\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">const</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">std</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--0fs:italic;--1:#FE8929\">@import</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"std\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">const</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">Vec3</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">struct</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69\">    </span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">x</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">f32</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69\">    </span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">y</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">f32</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69\">    </span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">z</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">f32</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">pub</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">fn</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">main</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">() </span><span style=\"--0:#7B34D9;--1:#FC8678\">void</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">const</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">inferred_constant</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">Vec3</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">undefined</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69\">    </span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">std</span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">debug</span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">print</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"Hello, {d}!</span><span style=\"--0:#8D477A;--1:#D995A8\">\\n</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">{</span><span style=\"--0:#4C4F69;--1:#91AFA3\">inferred_constant</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">})</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"const std = @import(&quot;std&quot;);const Vec3 = struct {    x: f32,    y: f32,    z: f32,};pub fn main() void {    const inferred_constant: Vec3 = undefined;    std.debug.print(&quot;Hello, {d}!\\n&quot;, .{inferred_constant});}\"><div></div></button></div></figure></div>\n<p>which prints:</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plaintext\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">➜ zig build run</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">Hello, Vec3{ .x = 0, .y = 0, .z = 0 }!</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"➜ zig build runHello, Vec3{ .x = 0, .y = 0, .z = 0 }!\"><div></div></button></div></figure></div>\n<p>I have clearly not gotten very far yet, but initial thoughts here: Rust’s trait-based implementation means I can customize the “default” values for my structs, which I’m not seeing in this implicit coercion yet. Guess we’ll find out soon whether or not this can be handled explicitly :D</p>\n<h2 id=\"arrays\">Arrays</h2>\n<p>The syntax for array declarations is quite clear and explicit, which I like. Notably, while you can do this:</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"zig\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">const</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">implicitly_sized_array</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> [</span><span style=\"--0:#4C4F69;--1:#91AFA3\">_</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">]</span><span style=\"--0:#7B34D9;--1:#FC8678\">u8</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">{}</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// _ means \"infer the size\"</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"const implicitly_sized_array = [_]u8{}; // _ means &quot;infer the size&quot;\"><div></div></button></div></figure></div>\n<p>you cannot have an inferred size reference as the type:</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"zig\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">const</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">implicitly_sized_array</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">[</span><span style=\"--0:#4C4F69;--1:#91AFA3\">_</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">]</span><span style=\"--0:#7B34D9;--1:#FC8678\">u8</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {}</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"const implicitly_sized_array[_]u8 = {};\"><div></div></button></div></figure></div>\n<p>Rust also <a href=\"https://play.rust-lang.org/?version=nightly&amp;mode=debug&amp;edition=2018&amp;gist=f27a1a0b20feebe3e6d0a3417f25ce45\">disallows this</a>, but the error is surprisingly worse than with Zig. Rust’s resident diagnostics magician Esteban <a href=\"https://twitter.com/ekuber/status/1393566561005314048\">assures me</a> this is a regression and is being tracked.</p>\n<p>The only problem I encountered here was that I can’t figure out how to print an array!</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"zig\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">const</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">implicitly_sized_array</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> [</span><span style=\"--0:#4C4F69;--1:#91AFA3\">_</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">]</span><span style=\"--0:#7B34D9;--1:#FC8678\">u8</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">{</span><span style=\"--0:#A54107;--1:#D995A8\">0</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">1</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">2</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">3</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#91AFA3\">std</span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">debug</span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">print</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"This is an array: {}</span><span style=\"--0:#8D477A;--1:#D995A8\">\\n</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">{</span><span style=\"--0:#4C4F69;--1:#91AFA3\">implicitly_sized_array</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">})</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Outputs: \"This is an array: \"</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"const implicitly_sized_array = [_]u8{0, 1, 2, 3};std.debug.print(&quot;This is an array: {}\\n&quot;, .{implicitly_sized_array});// Outputs: &quot;This is an array: &quot;\"><div></div></button></div></figure></div>\n<p>I found a <a href=\"https://github.com/ziglang/zig/pull/6870\">PR that overhauls formatting</a> but nothing there gave me any pointers on why my code doesn’t work. Hopefully this’ll get cleared up later.</p>\n<h2 id=\"if\">If</h2>\n<p>Nothing special here, aside from the early introduction to testing, which is slightly more pleasant than with Rust. I do however have qualms about the test output, which is unnecessarily noisy:</p>\n<div class=\"expressive-code\"><figure class=\"frame is-terminal\"><figcaption class=\"header\"><span class=\"title\"></span><span class=\"sr-only\">Terminal window</span></figcaption><pre data-language=\"shell\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">Test</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> [2/2] test </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">while with continue expression</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">... expected 2080, found 10</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">/nix/store/nhd75c4sr3l9wlaspilkwawx5ixkn74w-zig-0.7.1/lib/zig/std/testing.zig:74:32:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">0x206f9a</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">in</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">std.testing.expectEqual</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (test)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">                </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">std.debug.panic(</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">\"expected {}, found {}\"</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">.{</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">expected,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">actual</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">}</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">                               </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">^</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">/home/msfjarvis/git-repos/zig-playground/src/main.zig:29:24:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">0x205abd</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">in</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">test</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">while with continue expression</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (test)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">testing.expectEqual(sum,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">10</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">                       </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">^</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">/nix/store/nhd75c4sr3l9wlaspilkwawx5ixkn74w-zig-0.7.1/lib/zig/std/special/test_runner.zig:61:28:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">0x22e161</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">in</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">std.special.main</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (test)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">        </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">} </span><span style=\"--0:#7B34D9;--1:#FC8678\">else</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">test_fn.func</span><span style=\"--0:#5D5F6E;--1:#B4A694\">();</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">                           </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">^</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">/nix/store/nhd75c4sr3l9wlaspilkwawx5ixkn74w-zig-0.7.1/lib/zig/std/start.zig:334:37:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">0x20749d</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">in</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">std.start.posixCallMainAndExit</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (test)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">            </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">const</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">result</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">root.main</span><span style=\"--0:#5D5F6E;--1:#B4A694\">()</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">catch</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">|</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">err</span><span style=\"--0:#116B70;--1:#8EC07C\">|</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">                                    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">^</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">/nix/store/nhd75c4sr3l9wlaspilkwawx5ixkn74w-zig-0.7.1/lib/zig/std/start.zig:162:5:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">0x2071d2</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">in</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">std.start._start</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (test)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">@call(.</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">{</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">.modifier</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">.never_inline</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">},</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">posixCallMainAndExit,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">.{}</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">^</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">error:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">the</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">following</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">test</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">command</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">crashed:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">./src/zig-cache/o/5a647f3d3394214b30b3861a6b0ffbbb/test</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"Test [2/2] test &quot;while with continue expression&quot;... expected 2080, found 10/nix/store/nhd75c4sr3l9wlaspilkwawx5ixkn74w-zig-0.7.1/lib/zig/std/testing.zig:74:32: 0x206f9a in std.testing.expectEqual (test)                std.debug.panic(&quot;expected {}, found {}&quot;, .{ expected, actual });                               ^/home/msfjarvis/git-repos/zig-playground/src/main.zig:29:24: 0x205abd in test &quot;while with continue expression&quot; (test)    testing.expectEqual(sum, 10);                       ^/nix/store/nhd75c4sr3l9wlaspilkwawx5ixkn74w-zig-0.7.1/lib/zig/std/special/test_runner.zig:61:28: 0x22e161 in std.special.main (test)        } else test_fn.func();                           ^/nix/store/nhd75c4sr3l9wlaspilkwawx5ixkn74w-zig-0.7.1/lib/zig/std/start.zig:334:37: 0x20749d in std.start.posixCallMainAndExit (test)            const result = root.main() catch |err| {                                    ^/nix/store/nhd75c4sr3l9wlaspilkwawx5ixkn74w-zig-0.7.1/lib/zig/std/start.zig:162:5: 0x2071d2 in std.start._start (test)    @call(.{ .modifier = .never_inline }, posixCallMainAndExit, .{});    ^error: the following test command crashed:./src/zig-cache/o/5a647f3d3394214b30b3861a6b0ffbbb/test\"><div></div></button></div></figure></div>\n<h2 id=\"defer\">Defer</h2>\n<p>The <code>defer</code> language feature is something I’ve always been curious about since seeing it in Go, so I’m excited to discover use-cases for it when I finally have it available. Through ZigLearn I discovered that <code>defer</code> calls can be stacked to be executed in LIFO order, and Golang does it in the exact same fashion.</p>\n<h2 id=\"errors\">Errors</h2>\n<p>I like how easy it is to define errors, but the syntax feels kinda icky. Having each error enum I declare ‘magically’ become a property on the <code>error</code> keyword doesn’t sit right with me :(</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"zig\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">const</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">NumericError</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">error</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">{}</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FE8929\">fn</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">mayError</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#4C4F69;--1:#91AFA3\">shouldError</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">bool</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) </span><span style=\"--0:#7B34D9;--1:#FC8678\">anyerror</span><span style=\"--0:#116B70;--1:#8EC07C\">!</span><span style=\"--0:#7B34D9;--1:#FC8678\">u32</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">return</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FC8678\">if</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (</span><span style=\"--0:#4C4F69;--1:#91AFA3\">shouldError</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// This is different from what I'm accustomed to as a user of</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Either/Result type monads.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#7B34D9;--1:#FC8678\">error</span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">NumericError</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">else</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#A54107;--1:#D995A8\">10</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"const NumericError = error{};fn mayError(shouldError: bool) anyerror!u32 {    return if (shouldError)        // This is different from what I'm accustomed to as a user of        // Either/Result type monads.        error.NumericError    else        10;}\"><div></div></button></div></figure></div>\n<h2 id=\"runtime-safety\">Runtime Safety</h2>\n<p>Being able to turn off runtime safety features (like bounds checking) in specific blocks is pretty interesting! Not sure if I’ll ever have a valid use for it though…</p>\n<h1 id=\"conclusion\">Conclusion</h1>\n<p>I like most of what I’ve seen so far in Zig. Aside from the issues I mentioned above, the lack of string type is a very confusing thing for me. I’ve kinda come to expect it everywhere based on my previous experiences with Python, Java, Kotlin, and Rust; but maybe I’ll now learn to appreciate how every character on my screen is just numbers :D.</p>\n<p>I was very easily distracted today, so I only made it a third of the way in 3 hours for a chapter that is supposed to take 1 hour for the whole thing. Hoping to finish it tomorrow!</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/first-steps-with-zig/",
      "url": "https://msfjarvis.dev/posts/first-steps-with-zig/",
      "title": "First steps with Zig",
      "date_published": "2021-05-14T06:30:00.000Z",
      "summary": "I've decided to learn Zig, and here's how I'm preparing for it.",
      "content_html": "<p><a href=\"https://ziglang.org\">Zig</a> is a systems programming language much akin to <a href=\"https://rust-lang.org\">Rust</a> and C, and has been showing up in my feeds a lot as of late. Many Zig programmers have <a href=\"https://kevinlynagh.com/rust-zig/\">documented their experience with Zig</a> as much better than with Rust, which I have been programming in for the last year or so, citing simplicity and ease. I tend to agree that Rust can often be <em>complex</em> to enforce the guarantee of being <em>correct</em>, so I set out to finally buy into the promise of Zig and give it a shot.</p>\n<h1 id=\"compiler-and-ide-setup\">Compiler and IDE setup</h1>\n<p>The <a href=\"https://ziglang.org/learn/getting-started/#installing-zig\">installing Zig</a> page recommends that while using the Zig stable releases is fine for evaluating it, their <a href=\"https://ziglang.org/learn/getting-started/#tagged-release-or-nightly-build\">stable release cadence</a> matches LLVM’s ~6 months which means they are often rendered outdated by the fast pace of Zig development.</p>\n<p>Since I wanted to stick with using <a href=\"https://nixos.org/\">Nix</a> to manage my (currently) temporary Zig environment, I went with the stable 0.7.1 release available on nixpkgs.</p>\n<p>A quick <code>nix-shell -p zig</code> later, I now had access to the Zig compiler.</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame is-terminal\"><figcaption class=\"header\"><span class=\"title\"></span><span class=\"sr-only\">Terminal window</span></figcaption><pre data-language=\"shell\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">➜</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">nix-shell</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#D995A8\">-p</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">zig</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">➜</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">zig</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">version</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">0.7.1</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"➜ nix-shell -p zig➜ zig version0.7.1\"><div></div></button></div></figure></div>\n<p>To be able to use VSCode for writing Zig, I also installed the official <a href=\"https://github.com/zigtools/zls\">zls</a> language server for Zig. This did get me go-to-declaration support for the standard library, <del>but not syntax highlighting. I’m not sure if that’s intended, or a bug with my local setup</del>. Syntax highlighting is also present, thanks to Lewis Gaul for his suggestion of using the <code>tiehuis.zig</code> extension.</p>\n<h1 id=\"learning-resources\">Learning resources</h1>\n<p>The Zig team frankly admits that they do not yet have the resources to maintain extensive learning resources, but the Zig community has stepped forward to fill in those gaps. <a href=\"https://ziglearn.org/\">ziglearn.org</a> is a great jumping off point for people who prefer to learn language basics directly, and there is a <a href=\"https://github.com/rust-lang/rustlings\">rustlings</a> counterpart in <a href=\"https://github.com/ratfactor/ziglings\">ziglings</a> for learning by looking at code.</p>\n<p>On the official side of things, you get the <a href=\"https://ziglang.org/documentation/0.7.1/std/\">standard library reference</a> as one would expect, as well as a fairly detailed <a href=\"https://ziglang.org/documentation/0.7.1/\">language reference</a>.</p>\n<p>This is in contrast with Rust, which has an officially maintained <a href=\"https://doc.rust-lang.org/book/\">book</a> and maintains <a href=\"https://github.com/rust-lang/rustlings\">rustlings</a> as a first-party learning resource. They do however are a significantly larger and older team, so maybe with sufficient funding we’ll see Zig be able to devote effort towards this as well.</p>\n<h1 id=\"your-first-program\">Your first program</h1>\n<p>The <code>zig</code> CLI contains commands to generate new projects easily, so let’s create a new binary project.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"plaintext\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">➜ zig init-exe</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">info: Created build.zig</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">info: Created src/main.zig</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4c4f69;--1:#ebdbb2\">info: Next, try `zig build --help` or `zig build run`</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"➜ zig init-exeinfo: Created build.ziginfo: Created src/main.ziginfo: Next, try `zig build --help` or `zig build run`\"><div></div></button></div></figure></div>\n<p>The <code>build.zig</code> file appears to describe to the <code>zig</code> CLI how to build this program, and <code>src/main.zig</code> is our application code. Here’s what <code>zig init-exe</code> gives you for a “hello world” program:</p>\n<div class=\"expressive-code\"><figure class=\"frame has-title\"><figcaption class=\"header\"><span class=\"title\">src/main.zig</span></figcaption><pre data-language=\"zig\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">const</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">std</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--0fs:italic;--1:#FE8929\">@import</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"std\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">pub</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">fn</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">main</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">() </span><span style=\"--0:#7B34D9;--1:#FC8678\">anyerror</span><span style=\"--0:#116B70;--1:#8EC07C\">!</span><span style=\"--0:#7B34D9;--1:#FC8678\">void</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69\">    </span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">std</span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">log</span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">info</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"All your codebase are belong to us.\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#B4A694\">.</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">{})</span><span style=\"--0:#5D5F6E;--1:#B4A694\">;</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"const std = @import(&quot;std&quot;);pub fn main() anyerror!void {    std.log.info(&quot;All your codebase are belong to us.&quot;, .{});}\"><div></div></button></div></figure></div>\n<p>Cheeky.</p>\n<p>This post is just a brief overview of how I went about setting things up for learning Zig. I intend to post more detailed blogs as I progress :)</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/github-actions-tips-tricks/",
      "url": "https://msfjarvis.dev/posts/github-actions-tips-tricks/",
      "title": "Tips and Tricks for GitHub Actions",
      "date_published": "2021-01-02T00:00:00.000Z",
      "summary": "GitHub Actions is a power CI/CD platform that can do a lot more than your traditional CI systems. Here's some tips to get you started with exploring its true potential.",
      "content_html": "<p>GitHub Actions has grown at a rapid pace, and has become the CI platform of choice for most open source projects. The recent changes to Travis CI’s pricing for open source is certainly bound to accelerate this even more.</p>\n<p>Due to it being a first-party addition to GitHub, Actions has nearly infinite potential to run jobs in reaction to changes on GitHub. You can automatically set labels to newly opened pull requests, greet first time contributors, and more.</p>\n<p>Let’s go over some things that you can do with Actions, and we’ll end it with some safety related tips to ensure that your workflows are secure from both rogue action authors as well as rogue pull requests.</p>\n<h2 id=\"running-workflows-based-on-a-cron-trigger\">Running workflows based on a cron trigger</h2>\n<p>GitHub Actions can trigger the execution of a workflow in response to a large list of events as given <a href=\"https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows\">here</a>, one of them being a cron schedule. Let’s see how we can use the schedule feature to automate repetitive tasks.</p>\n<p>For <a href=\"https://msfjarvis.dev/aps\">Android Password Store</a>, we maintain a list of known <a href=\"https://publicsuffix.org/\">public suffixes</a> to be able efficiently detect the ‘base’ domain of the website we’re autofilling into. This list changes frequently, and we typically sync our repository with the latest copy on a weekly basis. Actions enables us to do this automatically:</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"yaml\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--1:#8EC07C\">name</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Update Publix Suffix List data</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#A54107;--1:#D995A8\">on</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#8EC07C\">schedule</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--1:#B4A694\">-</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--1:#8EC07C\">cron</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">0 0 * * 6</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--1:#8EC07C\">jobs</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#8EC07C\">update-publicsuffix-data</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># The actual workflow doing the update job</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"name: Update Publix Suffix List dataon:  schedule:    - cron: &quot;0 0 * * 6&quot;jobs:  update-publicsuffix-data:  # The actual workflow doing the update job\"><div></div></button></div></figure></div>\n<p>Putting the cron expression into <a href=\"https://crontab.guru/#0_*_*_*_6\">crontab guru</a>, you can see that it executes at 12AM on every Saturday. Going through the merged pull requests in APS, you will also notice that the <a href=\"https://github.com/android-password-store/Android-Password-Store/pulls?q=is%3Apr+is%3Amerged+sort%3Aupdated-desc+label%3APSL\">publicsuffixlist pull requests</a> indeed happen no sooner than 7 days apart.</p>\n<p>Mine is a very naive example of how you can use cron triggers to automate parts of your workflow. The <a href=\"https://github.com/rust-lang\">Rust</a> project uses these same triggers to implement a significantly more important aspect of their daily workings. Rust maintains a repository called <a href=\"https://github.com/rust-lang/glacier\">glacier</a> which contains a list of internal compiler errors (ICEs) and code fragments to reproduce each of them. Using a similar cron trigger, this repository checks each new nightly release of Rust to see if any of these compiler crashes were resolved silently by a refactor. When it comes across a ICE that was fixed (compiles correctly or fails with errors rather than crashing the compiler), it files a <a href=\"https://github.com/rust-lang/glacier/pulls?q=is%3Apr+author%3Aapp%2Fgithub-actions+sort%3Aupdated-desc\">pull request</a> moving the reproduction file to the <code>fixed</code> pile.</p>\n<h2 id=\"running-jobs-based-on-commit-message\">Running jobs based on commit message</h2>\n<p>Continuous delivery is great, but sometimes you want slightly more control. Rather than run a deployment task on each push to your repository, what if you want it to only run when a specific keyword is in the commit message? Actions has support for this natively, and the deployment pipeline of this very site relies on this feature:</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"yaml\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--1:#8EC07C\">name</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">Deploy to Cloudflare Workers Sites</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#A54107;--1:#D995A8\">on</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#8EC07C\">push</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#8EC07C\">branches</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">-</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">main</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--1:#8EC07C\">jobs</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#8EC07C\">deploy-main</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#8EC07C\">if</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">contains(github.event.head_commit.message, '[deploy]')</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Set up wrangler and push to the production environment</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#8EC07C\">deploy-staging</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#8EC07C\">if</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">contains(github.event.head_commit.message, '[staging]')</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Set up wrangler and push to the staging environment</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"name: Deploy to Cloudflare Workers Siteson:  push:    branches:      - mainjobs:  deploy-main:    if: &quot;contains(github.event.head_commit.message, '[deploy]')&quot;    # Set up wrangler and push to the production environment  deploy-staging:    if: &quot;contains(github.event.head_commit.message, '[staging]')&quot;    # Set up wrangler and push to the staging environment\"><div></div></button></div></figure></div>\n<p>This snippet defines a job that is only executed when the top commit of the push contains the text <code>[deploy]</code> in its message, and another that only runs when the commit message contains <code>[staging]</code>. Together, these let me control if I want a change to not be immediately deployed, deployed to either the main or staging site, or to both at the same time. So now I can update a draft post without a full re-deployment of the main site, or make a quick edit to a published post that doesn’t need to be reflected in the staging environment.</p>\n<p>The core logic of this operation is composed of three parts. The <a href=\"https://docs.github.com/en/free-pro-team@latest/actions/reference/context-and-expression-syntax-for-github-actions#github-context\">github context</a>, the <a href=\"https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#jobsjob_idif\">if conditional</a> and the <a href=\"https://docs.github.com/en/free-pro-team@latest/actions/reference/context-and-expression-syntax-for-github-actions#contains\">contains</a> method. The linked documentation for each does a great job at explaining them, and has further references to allow you to fulfill even more advanced use cases.</p>\n<h2 id=\"testing-across-multiple-configurations-in-parallel\">Testing across multiple configurations in parallel</h2>\n<p>Jobs in a workflow run in parallel by default, and GitHub comes with an amazing matrix functionality that can automatically generate multiple jobs for you from a single definition. Take this specific example:</p>\n<table>\n<thead>\n<tr>\n<th></th>\n<th>Windows</th>\n<th>MacOS</th>\n<th>Ubuntu</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Stable</td>\n<td>Windows + Stable</td>\n<td>MacOS + Stable</td>\n<td>Ubuntu + Stable</td>\n</tr>\n<tr>\n<td>Beta</td>\n<td>Windows + Beta</td>\n<td>MacOS + Beta</td>\n<td>Ubuntu + Beta</td>\n</tr>\n<tr>\n<td>Nightly</td>\n<td>Windows + Nightly</td>\n<td>MacOS + Nightly</td>\n<td>Ubuntu + Nightly</td>\n</tr>\n</tbody>\n</table>\n<blockquote>\n<sub><p>This particular matrix is for Rust, to test a codebase across Windows,\nUbuntu, and macOS using the Rust stable, beta, and nightly toolchains.</p></sub>\n</blockquote>\n<p>In GitHub Actions, we can simply provide the platforms (Windows, MacOS and, Ubuntu) and the Rust channels (Stable, Beta, and Nightly) inside a single job and let it figure out how to make the permutations and create separate jobs for them. To configure such a matrix, we write something like this:</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"yaml\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--1:#8EC07C\">jobs</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#8EC07C\">check-rust-code</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#8EC07C\">strategy</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Defines a matrix strategy</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--1:#8EC07C\">matrix</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Sets the OSes we want to run jobs on</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--1:#8EC07C\">os</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">ubuntu-latest</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">windows-latest</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">macOS-latest</span><span style=\"--0:#5D5F6E;--1:#B4A694\">]</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Sets the Rust channels we want to test against</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--1:#8EC07C\">rust</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">stable</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">beta</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">nightly</span><span style=\"--0:#5D5F6E;--1:#B4A694\">]</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Make the job run on the OS picked by the matrix</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#8EC07C\">runs-on</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">${{ matrix.os }}</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#8EC07C\">steps</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">-</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--1:#8EC07C\">uses</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">actions-rs/toolchain@v1</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--1:#8EC07C\">with</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">          </span><span style=\"--0:#1A57D1;--1:#8EC07C\">profile</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">minimal</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">          </span><span style=\"--0:#1A57D1;--1:#8EC07C\">components</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">rustfmt, clippy</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">          </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Installs the Rust toolchain for the channel picked by the matrix</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">          </span><span style=\"--0:#1A57D1;--1:#8EC07C\">toolchain</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">${{ matrix.rust }}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"jobs:  check-rust-code:    strategy:      # Defines a matrix strategy      matrix:        # Sets the OSes we want to run jobs on        os: [ubuntu-latest, windows-latest, macOS-latest]        # Sets the Rust channels we want to test against        rust: [stable, beta, nightly]    # Make the job run on the OS picked by the matrix    runs-on: ${{ matrix.os }}    steps:      - uses: actions-rs/toolchain@v1        with:          profile: minimal          components: rustfmt, clippy          # Installs the Rust toolchain for the channel picked by the matrix          toolchain: ${{ matrix.rust }}\"><div></div></button></div></figure></div>\n<p>This will automatically generate 9 (3 platforms * 3 Rust channels) parallel jobs to test this entire configuration, without requiring us to manually define each of them. <a href=\"https://en.wikipedia.org/wiki/Don%27t_repeat_yourself\">DRY</a> at its finest :)</p>\n<h2 id=\"make-a-job-run-after-another\">Make a job run after another</h2>\n<p>By default, jobs defined in a workflow file run in parallel. However, we might need a more sequential order of execution for some cases, and GHA does include support for this case. Let’s try another real world example!</p>\n<p><a href=\"https://github.com/square/leakcanary\">LeakCanary</a> has a <a href=\"https://github.com/square/leakcanary/blob/f5343aca6e019994f7e69a28fac14ca18e071b88/.github/workflows/main.yml\">checks job</a> that runs on each push to the main branch and on each pull request. They wanted to add support for snapshot deployment, in order to finally retire Travis CI. To make this happen, I simply added a <a href=\"https://github.com/square/leakcanary/pull/2044/commits/a6f6c204559396120836b27c0b2a46d3e444c728\">new job</a> to the same workflow, having it run only on push events and have a dependency on the checks job. This ensures that there won’t be a snapshot deployment until all tests are passing on the main branch. The relevant parts of the workflow configuration are here:</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"yaml\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#A54107;--1:#D995A8\">on</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#8EC07C\">pull_request</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#8EC07C\">push</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--1:#8EC07C\">branches</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--1:#B4A694\">-</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">main</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--1:#8EC07C\">jobs</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#8EC07C\">checks</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Runs automated unit and instrumentation tests</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#8EC07C\">snapshot-deployment</span><span style=\"--0:#116B70;--1:#B4A694\">:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Only run if the push event triggered this workflow run</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#8EC07C\">if</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">github.event_name == 'push'</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># Run after the 'checks' job has passed</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--1:#8EC07C\">needs</span><span style=\"--0:#116B70;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">[</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">checks</span><span style=\"--0:#5D5F6E;--1:#B4A694\">]</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"on:  pull_request:  push:    branches:      - mainjobs:  checks:  # Runs automated unit and instrumentation tests  snapshot-deployment:  # Only run if the push event triggered this workflow run  if: &quot;github.event_name == 'push'&quot;  # Run after the 'checks' job has passed  needs: [checks]\"><div></div></button></div></figure></div>\n<h1 id=\"mitigating-security-concerns-with-actions\">Mitigating security concerns with Actions</h1>\n<p>GitHub Actions benefits from a vibrant ecosystem of user-authored actions, which opens it up to equal opportunities for abuse. It is relatively easy to work around the common ones, and I’m going to outline them here. I’m no authority on security, and these recommendations are based on a combination of my reading and understanding. These <em>should</em> be helpful, but this list is not exhaustive, and you should exercise all the caution you can.</p>\n<h2 id=\"use-exact-commit-hashes-rather-than-tags\">Use exact commit hashes rather than tags</h2>\n<p>Tags are moving qualifiers, and can be <a href=\"https://julienrenaux.fr/2019/12/20/github-actions-security-risk/\">force pushed at any moment</a>. If the repository for an Action you use in your workflows is compromised, the tag you use could be force pushed with a malicious version that can send your repository secrets to a third-party server. Auditing the source of a repository at a given tag, then using the SHA1 commit hash it currently points to as the version addresses that concern due to it being nearly impossible to fake a new commit with the exact hash.</p>\n<p>To get the commit hash for a specific tag, head to the Releases page of the repository, then click the short SHA1 hash below the tag name and copy the full hash from the URL.</p>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/actions_tips_tricks_commit_hash.CdN2aTAL.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/actions_tips_tricks_commit_hash.CdN2aTAL.webp\" data-pswp-width=\"1166\" data-pswp-height=\"412\" aria-label=\"A tag along with its commit hash\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/actions_tips_tricks_commit_hash.CdN2aTAL.webp\" alt=\"A tag along with its commit hash\" loading=\"lazy\" decoding=\"async\" width=\"1166\" height=\"412\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a></figure>\n<blockquote>\n<sub><p>Here, the commit hash is feb985e. Ideally, you want to click that link and\ncopy the full hash from the URL</p></sub>\n</blockquote>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">job:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">checks:</span></div></div><div class=\"ec-line highlight del\"><div class=\"code\"><span style=\"--0:#484b64\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">   </span></span><span style=\"--1:#EBDBB2\">- uses: burrunan/gradle-cache-actions@v1.6</span></span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">+  - uses: burrunan/gradle-cache-actions@feb985ecf49f57f54f31920821a50d0394faf122</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"job:  checks:   - uses: burrunan/gradle-cache-actions@v1.6 +  - uses: burrunan/gradle-cache-actions@feb985ecf49f57f54f31920821a50d0394faf122\"><div></div></button></div></figure></div>\n<h3 id=\"alternate-solution\">Alternate solution</h3>\n<p>A more extreme fix for this problem is to <a href=\"https://stackoverflow.com/questions/26217488/what-is-vendoring\">vendor</a> each third-party action you use into your own repository, and then use the local copy as the source. This puts you in charge of manually syncing the source to each version, but allows you to restrict the allowed Actions to ones in your repository thereby greatly increasing security. However, having to manually sync can get tedious if your workflows involve a lot of third-party actions. However, the same manual sync also gives you slightly better visibility into the changes between versions since they’d be available in a single PR diff.</p>\n<p>To use an Action from a local directory, replace the <code>uses:</code> line with the relative path to the local copy in the repository.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">job:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">checks:</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">- name: Checkout repository</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\"># Assuming the copy of actions/checkout is at .github/actions/checkout</span></div></div><div class=\"ec-line highlight del\"><div class=\"code\"><span style=\"--0:#484b64\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">   </span></span><span style=\"--1:#EBDBB2\">- uses: actions/checkout@v2</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">   </span></span><span style=\"--1:#EBDBB2\">- uses: ./.github/actions/checkout</span></span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"job:  checks:    - name: Checkout repository    # Assuming the copy of actions/checkout is at .github/actions/checkout   - uses: actions/checkout@v2   - uses: ./.github/actions/checkout\"><div></div></button></div></figure></div>\n<h2 id=\"replace-pull_request_target-with-pull_request\">Replace <code>pull_request_target</code> with <code>pull_request</code></h2>\n<p><a href=\"https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows#pull_request_target\"><code>pull_request_target</code></a> grants a PR access to a github token that can write to your repository, exposing your code to modification by a malicious third-party who simply needs to open a PR against your repository. Most people will already be using the safe <a href=\"https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows#pull_request\"><code>pull_request</code></a> event, but if you are not, audit your requirements for <code>pull_request_target</code> and make the switch.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line highlight del\"><div class=\"code\"><span style=\"--0:#484b64;--1:#EBDBB2\">on: [push, pull_request_target]</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66;--1:#EBDBB2\">on: [push, pull_request]</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"on: [push, pull_request_target]on: [push, pull_request]\"><div></div></button></div></figure></div>\n<hr>\n<p>I’m still learning about Actions, and there is a lot that I did not cover here. I highly encourage readers to refer the GitHub docs for <a href=\"https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions\">Workflow syntax</a> and <a href=\"https://docs.github.com/en/free-pro-team@latest/actions/reference/context-and-expression-syntax-for-github-actions\">Context and expressions syntax</a> to gain more knowledge of the workflow configuration capabilities. Let me know if you find something cool that I did not cover here!</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/manually-parsing-json-with-moshi/",
      "url": "https://msfjarvis.dev/posts/manually-parsing-json-with-moshi/",
      "title": "Manually parsing JSON with Moshi",
      "date_published": "2020-12-21T06:30:00.000Z",
      "summary": "Moshi is a fast and powerful JSON parsing library for the JVM and Android. Today we look into manually parsing JSON to and from Java/Kotlin classes",
      "content_html": "<h3 id=\"what-is-moshi\">What is Moshi?</h3>\n<p><a href=\"https://github.com/square/moshi\">Moshi</a> is a fast and powerful JSON parsing library for the JVM and Android, built by the former creators of Google’s Gson to address some of its shortcomings and to have an alternative that was actively maintained.</p>\n<p>Unlike Gson, Moshi has excellent Kotlin support and supports both reflection based parsing and a kapt-backed codegen backend that eliminates the runtime performance cost in favor of generating adapters during build time. The <code>kotlin-reflect</code> dependency required for doing reflection-based parsing can add up to 1.8 mB to the final binary, so it’s recommended to use the codegen method if possible.</p>\n<h3 id=\"what-is-an-adapter\">What is an adapter?</h3>\n<p>An adapter is Moshi-speak for a class that can convert JSON into an object and an instance of that object into JSON. There are multiple types of JSON adapters supported by Moshi. The first is the one demonstrated in their README which contains two methods annotated <code>@ToJson</code> and <code>@FromJson</code>. The former takes an instance of the object and returns a String, and the latter takes a String and returns an instance of the object. This is the simplest type, and should be used for non-complex types that typically can be represented in simpler forms. <a href=\"https://github.com/square/moshi#custom-type-adapters\">Here’s the example Moshi uses</a>, and should be all the introduction you need for this particular type.</p>\n<p>The other type is similar to what Moshi generates for its kapt-generated adapters, but leverages the <code>@ToJson</code>/<code>@FromJson</code> annotations. The method signatures here are a bit verbose, and these are the ones we’re going to try to build.</p>\n<h3 id=\"why-write-your-own-adapters\">Why write your own adapters?</h3>\n<p>Good question. Consider this example class:</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"kotlin\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">@JsonClass</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(generateAdapter </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">true</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">class</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">TextParts</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">val</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> heading: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">String</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, </span><span style=\"--0:#7B34D9;--1:#FC8678\">val</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> body: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">String</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">? </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--1:#D995A8\">null</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"@JsonClass(generateAdapter = true)class TextParts(val heading: String, val body: String? = null)\"><div></div></button></div></figure></div>\n<p>Pretty straightforward. The <code>JsonClass</code> annotation with <code>generateAdapter = true</code> will attempt to use the codegen backend to write an adapter automatically for this. Let’s try converting this to JSON.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"kotlin\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">val</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> text </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">TextParts</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"This is the heading\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"And this is the body\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">val</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> moshi </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> Moshi.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">Builder</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">().</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">build</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// TextPartsJsonAdapter was generated by the codegen backend</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">println</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">TextPartsJsonAdapter</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(moshi).</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">toJson</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(text))</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">{</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"heading\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">:</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"This is the heading\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">,</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"body\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">:</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"And this is the body\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"val text = TextParts(&quot;This is the heading&quot;, &quot;And this is the body&quot;)val moshi = Moshi.Builder().build()// TextPartsJsonAdapter was generated by the codegen backendprintln(TextPartsJsonAdapter(moshi).toJson(text)){&quot;heading&quot;:&quot;This is the heading&quot;,&quot;body&quot;:&quot;And this is the body&quot;}\"><div></div></button></div></figure></div>\n<p>What this means is, given a JSON object that looks like this</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"json\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">heading</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">This is the heading</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">body</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">And this is the body</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"{ &quot;heading&quot;: &quot;This is the heading&quot;, &quot;body&quot;: &quot;And this is the body&quot; }\"><div></div></button></div></figure></div>\n<p>We can get an instance of <code>TextParts</code> that looks like this</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"kotlin\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">val</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> text </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">TextParts</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"This is the heading\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"And this is the body\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"val text = TextParts(&quot;This is the heading&quot;, &quot;And this is the body&quot;)\"><div></div></button></div></figure></div>\n<p>Cool! Now, let’s make things unfortunate. Imagine your backend team is stretched thin, and due to a limitation with how they initially built their database schema, you can only get the above JSON in this form</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"json\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">heading</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">This is the heading</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">extras</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">{</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#1A57D1;--1:#89B28B\">body</span><span style=\"--0:#5D5F6E;--1:#B4A694\">\"</span><span style=\"--0:#5D5F6E;--1:#B4A694\">:</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#2C6E1E;--1:#91AFA3\">And this is the body</span><span style=\"--0:#2C6E1E;--1:#B4A694\">\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"{  &quot;heading&quot;: &quot;This is the heading&quot;,  &quot;extras&quot;: { &quot;body&quot;: &quot;And this is the body&quot; }}\"><div></div></button></div></figure></div>\n<p>If you try to parse this with the old <code>TextPartsJsonAdapter</code>, your app is going to crash, because the JSON and its Kotlin representation have diverged. The equivalent Kotlin for this new JSON is going to be something like this:</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"kotlin\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">@JsonClass</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(generateAdapter </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">true</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">class</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">Extras</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">val</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> body: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">String</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">? </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--1:#D995A8\">null</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">@JsonClass</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(generateAdapter </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">true</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">class</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">TextParts</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FC8678\">val</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> heading: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">String</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, </span><span style=\"--0:#7B34D9;--1:#FC8678\">val</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> extras: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">Extras</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">? </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--1:#D995A8\">null</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"@JsonClass(generateAdapter = true)class Extras(val body: String? = null)@JsonClass(generateAdapter = true)class TextParts(val heading: String, val extras: Extras? = null)\"><div></div></button></div></figure></div>\n<p>Many things changed here. Your direct access to the <code>body</code> field now needs to go through <code>extras</code>, which just isn’t that nice. You’re also now incurring the (albeit miniscule) overhead of generating two adapters rather than one. Wouldn’t it be great if we could continue to have a flat object like before? Let’s try to make that happen.</p>\n<h3 id=\"how-to-write-your-own-moshi-adapter\">How to write your own Moshi adapter?</h3>\n<p>With less effort than one might think! Let’s put down the basic building blocks.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"kotlin\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">class</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">TextPartsJsonAdapter</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Moshi is flexible about the parameters of these two methods, and for simpler types</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// you will find it easier to follow the example from the Moshi README which does not</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// use JsonReader/JsonWriter and instead directly converts items to and from their String</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// representations. The method names are also not enforced, as Moshi only uses the</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// annotations to find relevant methods. The internal implementation of how they do it</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// can be found here: https://git.io/JLwnb</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">@FromJson</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">fun</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">fromJson</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(reader: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">JsonReader</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">): </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">TextParts</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">? {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">TODO</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"Not implemented\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">@ToJson</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">fun</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">toJson</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(writer: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">JsonWriter</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">TextParts</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">?) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">TODO</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"Not implemented\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"class TextPartsJsonAdapter {  // Moshi is flexible about the parameters of these two methods, and for simpler types  // you will find it easier to follow the example from the Moshi README which does not  // use JsonReader/JsonWriter and instead directly converts items to and from their String  // representations. The method names are also not enforced, as Moshi only uses the  // annotations to find relevant methods. The internal implementation of how they do it  // can be found here: https://git.io/JLwnb  @FromJson  fun fromJson(reader: JsonReader): TextParts? {    TODO(&quot;Not implemented&quot;)  }  @ToJson  fun toJson(writer: JsonWriter: value: TextParts?) {    TODO(&quot;Not implemented&quot;)  }}\"><div></div></button></div></figure></div>\n<p>Now we’re ready to start parsing. First, let’s implement the <code>toJson</code> part, where we take an instance of the object and then try to write the equivalent JSON for it. Since this is comparatively easier, I’m going to do it in one go and leave comments inline to explain what’s happening.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"kotlin\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">@ToJson</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">fun</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">toJson</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(writer: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">JsonWriter</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">TextParts</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">?) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Null values shouldn't arrive to the adapter, this error lets callers know</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// what builder options need to be passed to the Moshi.Builder() instance</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// to avoid this particular situation.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">if</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (</span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">==</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--1:#D995A8\">null</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">throw</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">NullPointerException</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"value was null! Wrap in .nullSafe() to write nullable values.\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Use the Kotlin `with` scoping method so we don't need to call</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// all methods with the `writer.` prefix.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">with</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(writer) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Start the JSON object.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">beginObject</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Since our `extras` field is nullable, and our backend will send</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// it as a literal null rather than skip it, we want null values to</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// be written into the final JSON.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">serializeNulls </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">true</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Create a JSON field with the name 'heading'</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">name</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"heading\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Set the value of the 'heading' field to the actual heading</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.heading)</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Create the 'extras' field</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">name</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"extras\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">if</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (</span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.body </span><span style=\"--0:#116B70;--1:#8EC07C\">!=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--1:#D995A8\">null</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// If the body text exists, then start a new object and add a</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// body field</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">beginObject</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">name</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"body\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.bodyText)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">endObject</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">} </span><span style=\"--0:#7B34D9;--1:#FC8678\">else</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Otherwise we put down a literal null</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">nullValue</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// End the top-level object.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">endObject</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"@ToJsonfun toJson(writer: JsonWriter: value: TextParts?) {  // Null values shouldn't arrive to the adapter, this error lets callers know  // what builder options need to be passed to the Moshi.Builder() instance  // to avoid this particular situation.  if (value == null) {    throw NullPointerException(&quot;value was null! Wrap in .nullSafe() to write nullable values.&quot;)  }  // Use the Kotlin `with` scoping method so we don't need to call  // all methods with the `writer.` prefix.  with(writer) {    // Start the JSON object.    beginObject()    // Since our `extras` field is nullable, and our backend will send    // it as a literal null rather than skip it, we want null values to    // be written into the final JSON.    serializeNulls = true    // Create a JSON field with the name 'heading'    name(&quot;heading&quot;)    // Set the value of the 'heading' field to the actual heading    value(value.heading)    // Create the 'extras' field    name(&quot;extras&quot;)    if (value.body != null) {      // If the body text exists, then start a new object and add a      // body field      beginObject()      name(&quot;body&quot;)      value(value.bodyText)      endObject()    } else {      // Otherwise we put down a literal null      nullValue()    }    // End the top-level object.    endObject()  }}\"><div></div></button></div></figure></div>\n<p>Parsing JSON manually is relatively easy to screw up and Moshi will let you know if you get nesting wrong (missed a closing <code>endObject()</code> or <code>endArray()</code>) and other easily detectable problems, but you should definitely have tests for all possible cases. I’ll let the readers do that on their own, but if you <em>really</em> need to see an example then let me know below.</p>\n<p>Anyways, that’s the object -&gt; JSON part sorted. Now let’s try to do the reverse. Here’s where we are as of now.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"kotlin\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">fun</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">fromJson</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(reader: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">JsonReader</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">): </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">TextParts</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">? {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">TODO</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"Not implemented\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"fun fromJson(reader: JsonReader): TextParts? {  TODO(&quot;Not implemented&quot;)}\"><div></div></button></div></figure></div>\n<p>Same as writing JSON, we need to start by making an object.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">fun fromJson(reader: JsonReader): TextParts? {</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">// We'll be constructing the object at the end so these</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">// will store the values we read.</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">var heading: String? = null</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">var body: String? = null</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">with(reader) {</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">beginObject()</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">endObject()</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">}</span></span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">TODO(\"Not implemented\")</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"fun fromJson(reader: JsonReader): TextParts? {  // We'll be constructing the object at the end so these  // will store the values we read.  var heading: String? = null  var body: String? = null  with(reader) {    beginObject()    endObject()  }  TODO(&quot;Not implemented&quot;)}\"><div></div></button></div></figure></div>\n<p>We have a fixed set of keys that we expect to read, so go ahead and configure a couple instances of <code>JsonReader.Options</code> that we will use to find the keys in this JSON.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66;--1:#EBDBB2\">val topLevelKeys = JsonReader.Options.of(\"heading\", \"extras\")</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66;--1:#EBDBB2\">val extrasKeys = JsonReader.Options.of(\"body\")</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">fun fromJson(reader: JsonReader): TextParts? {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">// We'll be constructing the object at the end so these</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">// will store the values we read.</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"val topLevelKeys = JsonReader.Options.of(&quot;heading&quot;, &quot;extras&quot;)val extrasKeys = JsonReader.Options.of(&quot;body&quot;)fun fromJson(reader: JsonReader): TextParts? {  // We'll be constructing the object at the end so these  // will store the values we read.\"><div></div></button></div></figure></div>\n<p>And we’re set. You’ll see the significance of the Options objects now.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">var body: String? = null</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">with(reader) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">beginObject()</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">while(hasNext()) {</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">when(selectName(topLevelKeys)) {</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">      </span></span><span style=\"--1:#EBDBB2\">0 -&gt; heading = nextString() ?: throw Util.unexpectedNull(</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">        </span></span><span style=\"--1:#EBDBB2\">\"heading\",</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">        </span></span><span style=\"--1:#EBDBB2\">\"text\",</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">        </span></span><span style=\"--1:#EBDBB2\">this</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">      </span></span><span style=\"--1:#EBDBB2\">)</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">}</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">}</span></span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">endObject()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">TODO(\"Not implemented\")</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"var body: String? = nullwith(reader) {  beginObject()  while(hasNext()) {    when(selectName(topLevelKeys)) {      0 -> heading = nextString() ?: throw Util.unexpectedNull(        &quot;heading&quot;,        &quot;text&quot;,        this      )    }  }  endObject()}TODO(&quot;Not implemented&quot;)\"><div></div></button></div></figure></div>\n<p><code>reader.hasNext()</code> is going to continue iterating through the document’s tokens until it’s completed, which lets us look through the entire document for the parts we need. The <code>selectName(JsonReader.Options)</code> method will return the index of a matched key, so <code>0</code> there means that the <code>heading</code> key was found. In response to that, we want to read it as a string and throw if it is null (since it’s non-nullable in <code>TextParts</code>). The <code>Util.unexpectedNull</code> method is a little nicety that is part of Moshi’s internals and is used by its kapt-generated adapters to provide better error messages and we’re going to do the same.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">0 -&gt; heading = nextString() ?: throw Util.unexpectedNull(</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">\"heading\",</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">\"text\",</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">this</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">-1 -&gt; {</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">      </span></span><span style=\"--1:#EBDBB2\">// Skip unknown values</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">      </span></span><span style=\"--1:#EBDBB2\">skipName()</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">      </span></span><span style=\"--1:#EBDBB2\">skipValue()</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">}</span></span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">endObject()</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"    0 -> heading = nextString() ?: throw Util.unexpectedNull(      &quot;heading&quot;,      &quot;text&quot;,      this    )    -1 -> {      // Skip unknown values      skipName()      skipValue()    }  }}endObject()\"><div></div></button></div></figure></div>\n<p>When I said that <code>selectName</code> returns the index of the matched key, I didn’t mention that it returns -1 when it comes across a key that isn’t in the Options object. Since we don’t care about them, we’re going to skip both their name and value and continue right on ahead. Now, we’re going to try and parse that inner <code>extras</code> object. A lot is about to happen quickly, but bear with me as I explain things.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">\"text\",</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">this</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66;--1:#EBDBB2\">1 -&gt; {</span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">// \"extras\" is nullable, so we first try to see if it is null.</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">// If it isn't, this will throw and we can then safely assume</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">// a non-null value and proceed.</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">try {</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">nextNull&lt;Any&gt;()</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">} catch (_: JsonDataException) {</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">beginObject()</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">while (hasNext()) {</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">      </span></span><span style=\"--1:#EBDBB2\">when (selectName(extrasKeys)) {</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">        </span></span><span style=\"--1:#EBDBB2\">0 -&gt; body = nextString()</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">        </span></span><span style=\"--1:#EBDBB2\">-1 -&gt; {</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">          </span></span><span style=\"--1:#EBDBB2\">// Skip unknown values</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">          </span></span><span style=\"--1:#EBDBB2\">skipName()</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">          </span></span><span style=\"--1:#EBDBB2\">skipValue()</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">        </span></span><span style=\"--1:#EBDBB2\">}</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">      </span></span><span style=\"--1:#EBDBB2\">}</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">}</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">    </span></span><span style=\"--1:#EBDBB2\">endObject()</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">}</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--1:#B4A694\">-</span><span style=\"--0:#C00E34;--1:#E49663\">1 -&gt; {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">// Skip unknown values</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">skipName()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">skipValue()</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"  &quot;text&quot;,  this)1 -> {  // &quot;extras&quot; is nullable, so we first try to see if it is null.  // If it isn't, this will throw and we can then safely assume  // a non-null value and proceed.  try {    nextNull<Any>()  } catch (_: JsonDataException) {    beginObject()    while (hasNext()) {      when (selectName(extrasKeys)) {        0 -> body = nextString()        -1 -> {          // Skip unknown values          skipName()          skipValue()        }      }    }    endObject()  }}-1 -> {  // Skip unknown values  skipName()  skipValue()\"><div></div></button></div></figure></div>\n<p>Now that you look at it, not really that different from what we did above. The only new thing here is the <code>nextNull</code> method, which simply tries to find a null value and throws the <code>JsonDataException</code> if the value wasn’t null.</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"diff\"><code><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">endObject()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line highlight del\"><div class=\"code\"><span style=\"--0:#484b64\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">TODO(\"Not implemented\")</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">// Satisfy the typechecker and throw in case the JSON body</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">// didn't contain the 'heading' field at all</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">require(heading != null) { \"heading must not be null\" }</span></span></div></div><div class=\"ec-line highlight ins\"><div class=\"code\"><span style=\"--0:#4a4d66\"><span class=\"indent\"><span style=\"--1:#EBDBB2\">  </span></span><span style=\"--1:#EBDBB2\">return TextParts(heading, body)</span></span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"    }    endObject()  }  TODO(&quot;Not implemented&quot;)  // Satisfy the typechecker and throw in case the JSON body  // didn't contain the 'heading' field at all  require(heading != null) { &quot;heading must not be null&quot; }  return TextParts(heading, body)}\"><div></div></button></div></figure></div>\n<p>And that’s it! The final adapter is going to look like this</p>\n<div class=\"expressive-code\"><figure class=\"frame\"><figcaption class=\"header\"></figcaption><pre data-language=\"kotlin\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FC8678\">class</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">TextPartsJsonAdapter</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">val</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> topLevelKeys </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> JsonReader.Options.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">of</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"heading\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">, </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"extras\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">val</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> extrasKeys </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> JsonReader.Options.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">of</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"body\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">@FromJson</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">fun</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">fromJson</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(reader: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">JsonReader</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">): </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">TextParts</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">? {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// We'll be constructing the object at the end so these</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// will store the values we read.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">var</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> heading: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">String</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">? </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--1:#D995A8\">null</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">var</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> body: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">String</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">? </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--1:#D995A8\">null</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">with</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(reader) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">beginObject</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#7B34D9;--1:#FC8678\">while</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">hasNext</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#7B34D9;--1:#FC8678\">when</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">selectName</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(topLevelKeys)) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">          </span><span style=\"--0:#A54107;--1:#D995A8\">0</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">-&gt;</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> heading </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">nextString</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">() ?: </span><span style=\"--0:#7B34D9;--1:#FC8678\">throw</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> Util.</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">unexpectedNull</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">            </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"heading\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">            </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"text\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">,</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">            </span><span style=\"--0:#C00E34;--1:#91AFA3\">this</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">          </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">          </span><span style=\"--0:#A54107;--1:#D995A8\">1</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">-&gt;</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">            </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// \"extras\" is nullable, so we first try to see if it is null.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">            </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// If it isn't, this will throw and we can then safely assume</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">            </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// a non-null value and proceed.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">            </span><span style=\"--0:#7B34D9;--1:#FC8678\">try</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">              </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">nextNull</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">&lt;</span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">Any</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">&gt;()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">            </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">} </span><span style=\"--0:#7B34D9;--1:#FC8678\">catch</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (_: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">JsonDataException</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">              </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">beginObject</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">              </span><span style=\"--0:#7B34D9;--1:#FC8678\">while</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">hasNext</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">                </span><span style=\"--0:#7B34D9;--1:#FC8678\">when</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (</span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">selectName</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(extrasKeys)) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">                  </span><span style=\"--0:#A54107;--1:#D995A8\">0</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">-&gt;</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> body </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">nextString</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">                  </span><span style=\"--0:#7B34D9;--1:#FC8678\">else</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">-&gt;</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">                    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Skip unknown</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">                    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">skipName</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">                    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">skipValue</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">                  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">                </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">              </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">              </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">endObject</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">            </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">          </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">          </span><span style=\"--0:#116B70;--1:#8EC07C\">-</span><span style=\"--0:#A54107;--1:#D995A8\">1</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#7B34D9;--1:#FE8929\">-&gt;</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">            </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">skipName</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">            </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">skipValue</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">          </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">        </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">endObject</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Satisfy the typechecker and throw in case the JSON body</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// didn't contain the 'heading' field at all</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">require</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(heading </span><span style=\"--0:#116B70;--1:#8EC07C\">!=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--1:#D995A8\">null</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) { </span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"heading must not be null\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> }</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">return</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">TextParts</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(heading, body)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">@ToJson</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">  </span><span style=\"--0:#7B34D9;--1:#FC8678\">fun</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">toJson</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(writer: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">JsonWriter</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">: </span><span style=\"--0:#865511;--0fs:italic;--1:#FABD2F\">TextParts</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">?) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Null values shouldn't arrive to the adapter, this error lets callers know</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// what builder options need to be passed to the Moshi.Builder() instance</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// to avoid this particular situation.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#7B34D9;--1:#FC8678\">if</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (</span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#116B70;--1:#8EC07C\">==</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--1:#D995A8\">null</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#7B34D9;--1:#FC8678\">throw</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">NullPointerException</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"value was null! Wrap in .nullSafe() to write nullable values.\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Use the Kotlin `with` scoping method so we don't need to call</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// all methods with the `writer.` prefix.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">    </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">with</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(writer) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Start the JSON object.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">beginObject</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Since our `extras` field is nullable, and our backend will send</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// it as a literal null rather than skip it, we want null values to</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// be written into the final JSON.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">serializeNulls </span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#A54107;--1:#D995A8\">true</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Create a JSON field with the name 'heading'</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">name</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"heading\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Set the value of the 'heading' field to the actual heading</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.heading)</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Create the 'extras' field</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">name</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"extras\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#7B34D9;--1:#FC8678\">if</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> (</span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.body </span><span style=\"--0:#116B70;--1:#8EC07C\">!=</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#C00E34;--1:#D995A8\">null</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">) {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// If the body text exists, then start a new object and add a body field</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">beginObject</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">name</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">\"body\"</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">(</span><span style=\"--0:#7B34D9;--1:#FE8929\">value</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">.bodyText)</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">endObject</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">} </span><span style=\"--0:#7B34D9;--1:#FC8678\">else</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> {</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// Otherwise we put down a literal null</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">        </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">nullValue</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">      </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\">\n</div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\">// End the top-level object.</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\">      </span><span style=\"--0:#1A57D1;--0fs:italic;--1:#FABD2F\">endObject</span><span style=\"--0:#4C4F69;--1:#EBDBB2\">()</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">    </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span class=\"indent\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">  </span></span><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#EBDBB2\">}</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"class TextPartsJsonAdapter {  val topLevelKeys = JsonReader.Options.of(&quot;heading&quot;, &quot;extras&quot;)  val extrasKeys = JsonReader.Options.of(&quot;body&quot;)  @FromJson  fun fromJson(reader: JsonReader): TextParts? {    // We'll be constructing the object at the end so these    // will store the values we read.    var heading: String? = null    var body: String? = null    with(reader) {      beginObject()      while(hasNext()) {        when(selectName(topLevelKeys)) {          0 -> heading = nextString() ?: throw Util.unexpectedNull(            &quot;heading&quot;,            &quot;text&quot;,            this          )          1 -> {            // &quot;extras&quot; is nullable, so we first try to see if it is null.            // If it isn't, this will throw and we can then safely assume            // a non-null value and proceed.            try {              nextNull<Any>()            } catch (_: JsonDataException) {              beginObject()              while (hasNext()) {                when (selectName(extrasKeys)) {                  0 -> body = nextString()                  else -> {                    // Skip unknown                    skipName()                    skipValue()                  }                }              }              endObject()            }          }          -1 -> {            skipName()            skipValue()          }        }      }      endObject()    }    // Satisfy the typechecker and throw in case the JSON body    // didn't contain the 'heading' field at all    require(heading != null) { &quot;heading must not be null&quot; }    return TextParts(heading, body)  }  @ToJson  fun toJson(writer: JsonWriter: value: TextParts?) {    // Null values shouldn't arrive to the adapter, this error lets callers know    // what builder options need to be passed to the Moshi.Builder() instance    // to avoid this particular situation.    if (value == null) {      throw NullPointerException(&quot;value was null! Wrap in .nullSafe() to write nullable values.&quot;)    }    // Use the Kotlin `with` scoping method so we don't need to call    // all methods with the `writer.` prefix.    with(writer) {      // Start the JSON object.      beginObject()      // Since our `extras` field is nullable, and our backend will send      // it as a literal null rather than skip it, we want null values to      // be written into the final JSON.      serializeNulls = true      // Create a JSON field with the name 'heading'      name(&quot;heading&quot;)      // Set the value of the 'heading' field to the actual heading      value(value.heading)      // Create the 'extras' field      name(&quot;extras&quot;)      if (value.body != null) {        // If the body text exists, then start a new object and add a body field        beginObject()        name(&quot;body&quot;)        value(value.bodyText)        endObject()      } else {        // Otherwise we put down a literal null        nullValue()      }      // End the top-level object.      endObject()    }  }}\"><div></div></button></div></figure></div>\n<p>This is certainly a lengthy job to do, and this blog post is a result of nearly 8 hours I spent writing JSON adapters by hand. Certainly not recommended if avoidable, but sometimes you just need to. When it comes to it, now you hopefully know how :)</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/aps-october-release/",
      "url": "https://msfjarvis.dev/posts/aps-october-release/",
      "title": "Android Password Store October release",
      "date_published": "2020-10-22T06:30:00.000Z",
      "summary": "Long form release notes for the Android Password Store October release",
      "content_html": "<p>We’re back with yet another release! As I shared earlier this month, this is going to our last release for a while. There’s a lot of work left to be done, and we’re simply not big enough a team to have these larger changes be done separately from our main development. We’ll still be doing bugfix releases if and when required, so please do file bug reports as and when you encounter issues.</p>\n<h2 id=\"new-features\">New features</h2>\n<h3 id=\"gpg-key-selection-added-to-onboarding\">GPG key selection added to onboarding</h3>\n<p>Creating a new store from the app previously created an unusable store, because we never configured a GPG key in the <code>.gpg-id</code> file. This has now been remedied in two ways: empty <code>.gpg-id</code> files are correctly handled as invalid and included in our quickfix solution, and creating a new store will now request you to select a key and then write it into the <code>.gpg-id</code> file. Here’s what the key selection screen looks like:</p>\n<figure data-image-modal-root=\"\" data-astro-cid-xekddyko=\"\"><a class=\"pswp-gallery-item image-modal__trigger\" href=\"https://msfjarvis.dev/_astro/aps-october-release-gpg-key-selection.4ZnJOVfQ.webp\" data-pswp-light-src=\"https://msfjarvis.dev/_astro/aps-october-release-gpg-key-selection.4ZnJOVfQ.webp\" data-pswp-width=\"536\" data-pswp-height=\"954\" aria-label=\"GPG key selection screen from the APS October release\" data-astro-cid-xekddyko=\"\"><span class=\"image-modal__media\" data-astro-cid-xekddyko=\"\"><picture data-astro-cid-xekddyko=\"\"><img src=\"https://msfjarvis.dev/_astro/aps-october-release-gpg-key-selection.4ZnJOVfQ.webp\" alt=\"GPG key selection screen from the APS October release\" loading=\"lazy\" decoding=\"async\" width=\"536\" height=\"954\" data-astro-cid-xekddyko=\"\"></picture><span class=\"image-modal__hint\" aria-hidden=\"true\" data-astro-cid-xekddyko=\"\">Expand image</span></span></a></figure>\n<h3 id=\"allow-configuring-an-https-proxy\">Allow configuring an HTTPS proxy</h3>\n<p>Before we close the gates on our regularly scheduled releases, our focus has been to address most longstanding issues and one of the major ones there has been <a href=\"https://github.com/android-password-store/Android-Password-Store/issues/163\">Proxy support</a>. This has now been added, and can be accessed from the settings screen. Unfortunately, there are still a few caveats with this current implementation that may or may not change in a future patch release:</p>\n<ul>\n<li>No SOCKS5 support</li>\n<li>Relatively unhelpful error messages when proxy connection fails</li>\n</ul>\n<h3 id=\"add-option-to-automatically-sync-repository\">Add option to automatically sync repository</h3>\n<p><del>This too, has been a <a href=\"https://github.com/android-password-store/Android-Password-Store/issues/277\">consistent request</a> in the past. While our implementation does not exactly match what was requested, we feel it’s good enough to be shipped. You now have the option to sync your repository on every launch to ensure things are always up-to-date when you get in the app.</del></p>\n<p>Due to multiple bugs, this feature has been rolled back in <a href=\"https://github.com/android-password-store/Android-Password-Store/releases/tag/v1.13.1\">v1.13.1</a>.</p>\n\n<h2 id=\"fixes\">Fixes</h2>\n<h3 id=\"improved-error-messaging\">Improved error messaging</h3>\n<p>For a large set of connection related errors, the failure message would simply be ‘Invalid remote: origin’. That is exactly as unhelpful as one might think, and now we try harder to extract the actual, more meaningful error message.</p>\n<h3 id=\"use-gits-default-user-and-email-when-none-are-configured\">Use Git’s default user and email when none are configured</h3>\n<p>We don’t force users to set a name and email before they make any changes requiring Git commits, but somewhere in the last couple releases we regressed our behavior around this. Rather than the <code>root &lt;root@localhost&gt;</code> committer, we were incorrectly using empty strings resulting in all commits being authored by <code>&amp;lt;&amp;gt;</code>. This has now been resolved, and your commit history will now be adorned by <code>root@localhost</code> once more (but seriously, just set your name and email already).</p>\n<h3 id=\"improvements-around-phishing-detection-ux\">Improvements around phishing detection UX</h3>\n<p>APS has had comprehensive phishing detection built into our Autofill since day one. Our phishing-resistant search will not show your <code>google.com</code> passwords when you try to fill into <code>goggle.com</code>, and if the signature of an application changes after you first filled a password into it, we will warn you about the change. There were a couple issues with the way this was happening.</p>\n<p>First, the phishing detection UI was a bit complicated, and also had some unreadable, black-on-dark text. Since this was never reported to us, I believe none of our users are being phished by their apps which is great news :) Regardless, it is now fixed.</p>\n<p>Secondly, some complexity with how Android’s Autofill APIs work resulted in the “no I’m not being phished, accept this new signature” case to not work correctly. This caused the user to be continually shown the phishing detection prompt until they force closed the target app and started it again. That’s cumbersome, so we’ve fixed it now. Cheers to Fabian for his stellar work as always!</p>\n<h3 id=\"conclusion\">Conclusion</h3>\n<p>As you can notice, this is a bit of a small release by our standards. Fabian’s been busy with his Ph.D. (!!) and the new job he’s starting at soon (!!), and me and Aditya have been busy with our day jobs as well. This doesn’t spell doom for the project (yet), but your financial contributions over on <a href=\"https://github.com/sponsors/msfjarvis\">GitHub Sponsors</a> and <a href=\"https://opencollective.com/Android-Password-Store\">OpenCollective</a> are now much more important than ever to sustain the project during this time via bountied issues and simply compensating the current crop of developers for their time.</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/aps-september-release/",
      "url": "https://msfjarvis.dev/posts/aps-september-release/",
      "title": "Android Password Store September release",
      "date_published": "2020-09-21T06:30:00.000Z",
      "summary": "Long form release notes for the Android Password Store September release",
      "content_html": "<p>Continuing with this new-ish tradition we have going, here are the detailed release notes for the <a href=\"https://github.com/Android-Password-Store/android-password-store/releases/tag/v1.12.0\">v1.12.0</a> release.</p>\n<blockquote>\n<p>Multiple important announcements at the end of the page, make sure to read the whole thing!</p>\n</blockquote>\n<h2 id=\"new-features\">New features</h2>\n<h3 id=\"extend-autofill-support-to-more-browsers\">Extend Autofill support to more browsers</h3>\n<p><a href=\"https://github.com/djpohly\">Devin J. Pohly</a> and <a href=\"https://github.com/rounakdatta\">Rounak Dutta</a> collectively contributed support for 3 new Chromium-based browsers: <a href=\"https://www.bromite.org/\">Bromite</a>, <a href=\"https://git.droidware.info/wchen342/ungoogled-chromium-android\">Ungoogled Chromium</a> and <a href=\"https://kiwibrowser.com/\">Kiwi</a>.</p>\n<h3 id=\"allow-sorting-by-recently-used\">Allow sorting by recently used</h3>\n<p>This feature was requested <a href=\"https://msfjarvis.dev/aps/issue/535\">a while ago</a> and was <a href=\"https://msfjarvis.dev/aps/pr/1031\">implemented by Alex Molinares</a> early in the cycle. The database that keeps track of the recently used passwords is always active, so if and when you switch to this sorting mode you’ll see everything already sorted based on your old usage patterns. Neat!</p>\n<h3 id=\"add-ability-to-view-git-commit-log\">Add ability to view Git commit log</h3>\n<p>Another, <a href=\"https://msfjarvis.dev/aps/issue/284\">even older</a> feature request has finally been addressed. This too, <a href=\"https://msfjarvis.dev/aps/pr/1056\">came from an external contributor</a> and was one of the best pull requests I have ever seen. It’s a great feature, and I thoroughly enjoyed the entire process of its inclusion.</p>\n<h3 id=\"ssh-key-generation-and-handling-improvements\">SSH key generation and handling improvements</h3>\n<p>The old SSH key generation has been <a href=\"https://msfjarvis.dev/aps/pr/1070\">scrapped and rewritten</a> to use a set of safer cryptographic curve options that span the distance between widely supported and very secure. The <a href=\"https://github.com/android-password-store/Android-Password-Store/wiki/Generate-SSH-Key\">wiki page</a> has been updated for these changes with information on how we’re securing access to the actual SSH keys, like storing the key file in the Android Keystore and requiring screen lock authentication before the key can be used.</p>\n<h3 id=\"fallback-authentication-for-ssh\">Fallback authentication for SSH</h3>\n<p>SSH servers are often configured to have multiple authentication methods, where you first attempt to authenticate with private keys and if that fails, fall back to passwords. This wasn’t previously supported in APS, which would quit after the first failure. We’ve changed that to now offer the option of entering a password if the server is configured to fall back to it.</p>\n<h3 id=\"rewritten-and-redesigned-onboarding-flow\">Rewritten and redesigned onboarding flow</h3>\n<p>In a multi-step refactoring process, the initial flow of setting up the app has been completely revamped. The internals were completely overhauled to improve stability, weed out some gnarly hacks, and make the whole thing easier to test and understand. Maintainer <a href=\"https://github.com/Skrilltrax\">Aditya Wasan</a> did a fabulous job giving the <a href=\"https://msfjarvis.dev/aps/pr/1099\">UI a facelift</a>. It’s real pretty now ✨</p>\n<h3 id=\"show-hidden-folders-now-also-shows-hidden-directories\">Show hidden folders now also shows hidden directories</h3>\n<p>Our old ‘Show hidden folders’ feature has now been simplified to show <em>all</em> hidden files and folders in the repository. It is intended to make it easier to perform trivial maintenance tasks that would normally require access to a PC.</p>\n<h2 id=\"bugfixes\">Bugfixes</h2>\n<h3 id=\"ssh-connection-problems-with-bitbucket\">SSH connection problems with Bitbucket</h3>\n<p>In our last major release, we included a change to <a href=\"https://msfjarvis.dev/aps/pr/1012\">re-use SSH connections</a> to speed up Git operations. This had an unfortunate side effect: Bitbucket users were unable to use SSH to connect to their repositories. Atlassian has been <a href=\"https://community.atlassian.com/t5/Bitbucket-questions/Can-t-repo-sync-anymore/qaq-p/354231\">aware of this problem</a> for quite some time now and did nothing about it, so we now include a <a href=\"https://msfjarvis.dev/aps/pr/1093\">helpful message and an internal workaround</a> when this particular type of error is encountered.</p>\n<h3 id=\"symlink-support\">Symlink support</h3>\n<p>While still potentially finicky, we’re now confident that this is ready to be shipped to all users without the risk of crashes.</p>\n<h3 id=\"assorted-ux-improvements\">Assorted UX improvements</h3>\n<p>As always, there are a handful of Quality of Life changes to make the app more enjoyable to use:</p>\n<ul>\n<li>When retrying password authentication, the option to see what you’re typing would be obscured by the error icon for wrong password. This has been remedied, and the error state will now be cleared as soon as you enter anything into the password field.</li>\n<li>Authentication modes will now be dynamically hidden and shown based on the URL’s schema so you’re aware of what methods you have for authentication for any given remote repository.</li>\n<li>Since decryption can sometimes take a couple seconds due to how OpenKeychain works, we now hide the action buttons at the top of the screen until the decrypt operation has completed since using the buttons before that can leave the app in an odd state.</li>\n<li>Users will be prompted if they need to provide a username in their URLs. For example, if your repository is at <code>https://github.com/john.doe/passwords</code>, you will have to change the URL to <code>https://john.doe@github.com/john.doe/passwords</code> for HTTPS authentication to work.</li>\n<li>If it appears that an SSH URL contains a custom port but does not specify the <code>ssh://</code> schema, the user will be prompted to accept a quickfix that does it for them.</li>\n<li>Pressing the save button is no longer necessary to save changes to authentication mode.</li>\n<li>TOTP values might sometimes be outdated because we always wait 30 seconds to generate a new one. Now the app will calculate the time left before the first generated value goes stale, generate a new one once it does, and then resume the 30 second cycle.</li>\n</ul>\n<p>There’s definitely more fixes here, but we ended up rewriting, breaking and fixing so many things for this release that it’s hard to tell what was actually broken in the previous release and what is just us fixing regressions during refactoring. We’ve been busy :)</p>\n<h2 id=\"important-announcements\">Important announcements</h2>\n<h3 id=\"autofill-parser-is-now-a-standalone-library\">Autofill parser is now a standalone library!</h3>\n<p>Our excellent Autofill capabilities are now bundled as a separate Android library and can be used by other password managers to improve their Autofill experiences. Detailed documentation will be coming over the next few days, keep an eye out <a href=\"https://github.com/android-password-store/Android-Password-Store/tree/develop/autofill-parser\">here</a> if it’s something you’re interested in.</p>\n<h3 id=\"rfc-for-removal-of-git-support-in-external-repos\">RFC for removal of Git support in external repos</h3>\n<p>Based on the issues raised in the repository and the support emails I’ve received, the maintainers have come to the conclusion that nearly all users who choose to store their pass repositories in their device storage or external SD card as opposed to the app’s private, hidden directory are not users of Git and rely on solutions like Syncthing and Nextcloud to keep the repository in sync with their other devices.</p>\n<p>As such, we are now in the process of removing Git support from these repositories. We’ve carefully evaluated how we want to do this, and have started with removing the ability to clone repositories to public storage in this release. If this doesn’t blow up in our faces, we will be completing the transition in v1.13.0. If you believe the change adversely affects your usage of the app, we wanna know! Drop a comment on <a href=\"https://msfjarvis.dev/aps/issue/1118\">GitHub</a> and we will do our best to either propose an alternative for your use case or entirely scrap our plans if we discover that our initial inferences were misguided.</p>"
    },
    {
      "id": "https://msfjarvis.dev/posts/tools-of-the-trade-sdkman/",
      "url": "https://msfjarvis.dev/posts/tools-of-the-trade-sdkman/",
      "title": "Tools of the trade: SDKMAN!",
      "date_published": "2020-09-02T06:30:00.000Z",
      "summary": "Bringing this series back on popular demand, we're here to talk about SDKMAN!",
      "content_html": "<p>In the fourth post of <a href=\"https://msfjarvis.dev/categories/tools-of-the-trade/\">this series</a>, we’re talking about <a href=\"https://sdkman.io\">SDKMAN!</a>.</p>\n<h2 id=\"what-is-sdkman\">What is SDKMAN?</h2>\n<p>SDKMAN is a <strong>SDK</strong> <strong>Man</strong>ager. By employing its CLI, you can install a plethora of JVM-related tools and SDKs to your computer, without ever needing root access.</p>\n<h2 id=\"why-do-i-use-it\">Why do I use it?</h2>\n<p>Since I primarily work with <a href=\"https://kotlinlang.org/\">Kotlin</a>, having an up-to-date copy of the Kotlin compiler becomes helpful for quickly testing code samples with its inbuilt REPL (<strong>R</strong>ead <strong>E</strong>valuate <strong>P</strong>rint <strong>L</strong>oop). I also use <a href=\"https://gradle.org\">Gradle</a> as my build tool of choice, and tend to stay up-to-date with their releases in my projects. Finally, to run all these Java-based tools, you’re gonna need Java itself. Linux distros tend to package very outdated versions of the JDK which becomes a hindrance when building standalone JVM apps that I want to use the latest Java APIs in. SDKMAN allows you to install Java from multiple sources including AdoptOpenJDK, Azulsystems’ Zulu and many more.</p>\n<p>The real kicker here is the fact that you can keep multiple versions of the same thing installed. Using Java 14 everywhere but a specific project breaks over Java 8? Just install it alongside!</p>\n<p>To make this side-by-side ability even more useful, SDKMAN let’s you create a <code>.sdkmanrc</code> file in a directory, and it will switch your currently active version of any installed tool to the version specified in the file. People following the series from the beginning might recall this sounds awfully like direnv, because it is. However, SDKMAN is noticeably slow when executing these changes, presumably because its tearing down an existing symlink and creating a new one. For that reason, SDKMAN ships with the <code>sdk_auto_env</code> feature (automatically parse <code>.sdkmanrc</code> when you change directories) off by default, requiring you to manually type <code>sdk env</code> each time you enter a directory where you have a <code>.sdkmanrc</code>.</p>\n<p>Since the auto env feature matches what direnv does, I just use it directly. So, rather than doing this:</p>\n<div class=\"expressive-code\"><link rel=\"stylesheet\" href=\"https://msfjarvis.dev/_astro/ec.cteup.css\"><figure class=\"frame has-title\"><figcaption class=\"header\"><span class=\"title\">.sdkmanrc</span></figcaption><pre data-language=\"bash\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#4C4F69;--1:#91AFA3\">java</span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">8.0.262-zulu</span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"java=8.0.262-zulu\"><div></div></button></div></figure></div>\n<p>I do:</p>\n<div class=\"expressive-code\"><figure class=\"frame has-title\"><figcaption class=\"header\"><span class=\"title\">.envrc</span></figcaption><pre data-language=\"bash\"><code><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># In bash, doing `${VARIABLE/src/dest}` replaces `src` with</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># `dest` in `${VARIABLE}`, but you still need to write it</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#5D5F6E;--0fs:italic;--1:#B1A69C;--1fs:italic\"># back manually (hence the `export`).</span></div></div><div class=\"ec-line\"><div class=\"code\"><span style=\"--0:#7B34D9;--1:#FE8929\">export</span><span style=\"--0:#4C4F69;--1:#EBDBB2\"> </span><span style=\"--0:#4C4F69;--1:#91AFA3\">JAVA_HOME</span><span style=\"--0:#116B70;--1:#8EC07C\">=</span><span style=\"--1:#B4A694\"><span style=\"--0:#2C6E1E\">\"</span><span style=\"--0:#4C4F69\">${</span></span><span style=\"--0:#4C4F69;--1:#91AFA3\">JAVA_HOME</span><span style=\"--0:#116B70;--1:#8EC07C\">/</span><span style=\"--0:#4C4F69;--1:#91AFA3\">current</span><span style=\"--0:#116B70;--1:#8EC07C\">/</span><span style=\"--0:#4C4F69;--1:#91AFA3\">8</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">0</span><span style=\"--0:#2C6E1E;--1:#B8BB26\">.</span><span style=\"--0:#4C4F69;--1:#91AFA3\">262-zulu</span><span style=\"--1:#B4A694\"><span style=\"--0:#4C4F69\">}</span><span style=\"--0:#2C6E1E\">\"</span></span></div></div></code></pre><div class=\"copy\"><div aria-live=\"polite\"></div><button title=\"Copy to clipboard\" data-copied=\"Copied!\" data-code=\"# In bash, doing `${VARIABLE/src/dest}` replaces `src` with# `dest` in `${VARIABLE}`, but you still need to write it# back manually (hence the `export`).export JAVA_HOME=&quot;${JAVA_HOME/current/8.0.262-zulu}&quot;\"><div></div></button></div></figure></div>\n<p>for the same end result, but a lot faster.</p>\n<p>And that’s really it! SDKMAN’s pretty neat, but for the most part it just stays out of your way and thus there’s not a lot to talk about it. The next post’s gonna be much more hands-on :-)</p>\n<p>This was part 4 of the <a href=\"https://msfjarvis.dev/categories/tools-of-the-trade/\">Tools of the trade</a> series.</p>"
    }
  ]
}