<?xml version="1.0" encoding="utf-8" standalone="yes"?><?xml-stylesheet href="/pretty-feed-v3.xsl" type="text/xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Posts on Harsh Shandilya</title>
    <link>https://msfjarvis.dev/posts/</link>
    <description>Recent content in Posts on Harsh Shandilya</description>
    <image>
      <title>Harsh Shandilya</title>
      <url>https://msfjarvis.dev/android-chrome-512x512.webp</url>
      <link>https://msfjarvis.dev/android-chrome-512x512.webp</link>
    </image>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <managingEditor>me@msfjarvis.dev (Harsh Shandilya)</managingEditor>
    <webMaster>me@msfjarvis.dev (Harsh Shandilya)</webMaster>
    <lastBuildDate>Sun, 05 Apr 2026 23:36:00 +0530</lastBuildDate><atom:link href="https://msfjarvis.dev/posts/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Weeknotes: Week #14 (2026)</title>
      <link>https://msfjarvis.dev/posts/weeknotes-week-14-2026/</link>
      <pubDate>Sun, 05 Apr 2026 23:36:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/weeknotes-week-14-2026/</guid>
      <description>Finally over my sickness!</description>
      <content:encoded><![CDATA[<p>Barely managing to squeeze this in thanks to a super packed Sunday.</p>
<h1 id="sickness-update">Sickness Update</h1>
<p>I finally got through the rough week of dealing with the Flu, and used that down time to catch up with TV shows I was behind on. Finished watching <a href="https://www.imdb.com/title/tt27444205">Paradise</a>, and caught up with <a href="https://www.imdb.com/title/tt18923754">Daredevil: Born Again</a> and <a href="https://www.imdb.com/title/tt6741278/">Invincible</a>. I also started reading <a href="https://www.couldshouldmightdont.com/">Could Should Might Don&rsquo;t</a>, my first non-fiction read in a long time. So far I&rsquo;m not completely sold on the whole premise, but the topics the author chooses to break down are well presented.</p>
<h1 id="games">Games!</h1>
<p>I&rsquo;ve also picked up some of my half-completed games while I was sick. I&rsquo;m almost done beating <a href="https://store.steampowered.com/app/3081840/Chrono_Gear_Warden_of_Time/">Chrono Gear: Warden of Time</a>. I also resumed playing <a href="https://store.steampowered.com/app/2420110/Horizon_Forbidden_West_Complete_Edition/">Horizon Forbidden West</a> but the clunky combat pissed me off again. I love the story too much to let this detract from my enjoyment of it, so I opted to cheese my way using a trainer mod for a boss that was genuinely anti-fun. I&rsquo;m not no-clipping around the place, just using invulnerability to get past a fight that feels more annoying than skill based. I&rsquo;ve only used it once so far,  maybe in the future I&rsquo;ll get too lazy and start using it for all bosses, who&rsquo;s to say. Either way I&rsquo;d rather enjoy the game this way than not play it at all.</p>
<h1 id="writing">Writing</h1>
<p>In my Flu-stricken stupor I completely forgot about <a href="https://aprilcools.club">April Cools</a> this year, and had to scramble at the last minute to put a <a href="https://msfjarvis.dev/posts/i-guess-i-cook-now/">post together</a>. Admittedly, I tried to cheat my way out of the spirit of the thing by having an LLM convert my rough outline to a post but I found the writing to be so genuinely sickening to read that I threw it all away and just tanked the effort of writing through a fever to get this out.</p>
<h1 id="work-and-career">Work and Career</h1>
<p>I got the increment I was expecting at work, which was nice. I took most days off while recovering from the Flu so not a whole lot got done, but I helped grease things along wherever I could manage to spare some time to review the documentation changes we were working on.</p>
<h1 id="miscellaneous-tech-things">Miscellaneous Tech Things</h1>
<p>I&rsquo;m planning to redo my room layout to open up some space in the center following Yash&rsquo;s example, so I had to get a multi-plug extension for my extension so I could neatly tuck away all the power cables that go into my — extension-ception. I didn&rsquo;t know <a href="https://www.amazon.in/Eveready-Everprotect-Multiplug-i4/dp/B0CQPDT6RT?nsdOptOutParam=true">this</a> was a thing!</p>
<p>I also worked a bit on <a href="https://github.com/msfjarvis/compose-lobsters">Claw</a>, my Android app for <a href="https://lobste.rs">Lobsters</a>. I removed a data migration path that had done its job in the past two releases, and added the very initial bits of login support in the app. Eventually the goal is for the app to be able to let you upvote and save posts directly to Lobsters instead of just the local store, and to maybe let you reply to comments/posts if I take on the daunting task of handling WYSIWYG Markdown input.</p>
<h1 id="people">People!</h1>
<p>The weekend was packed with a lot of socializing. <a href="https://blr.indiewebclub.org">IndieWebClub</a> on Saturday was a discussion focused session and we traded a lot of hot takes about editors, LLMs, writing tools and everything in between. It was a great time! Sadly I was not able to finish my writing draft within the allotted time, but I hope to have it out by next week.</p>
<p>On Sunday Yash and I met up with <a href="https://github.com/anunaym14">Anunay</a> and a friend of his for lunch at <a href="https://www.instagram.com/23rdstreet_pizza/">23rd Street Pizza</a> which was rather underwhelming. We meandered around a fair bit after that for some dessert and much needed steps, then got back to our place to chill for a bit. Aforementioned friend introduced us to <a href="https://boardgamegeek.com/boardgame/420087/flip-7">Flip 7</a>, which was a hilarious time with just 4 players and we fully lost track of time until it had been 3 hours. This game made me fully understand how gambling addictions come to be. The temptation to got for just one more flip is too damn high!</p>
<h1 id="the-food-section">The food section</h1>
<p>For this week I will choose to be lazy and delegate the responsibility for capturing our week&rsquo;s food to my roommate <a href="https://yashgarg.dev/">Yash</a>, who is far more diligent about this than I can ever hope to be. Check out his <a href="https://yashgarg.dev/weekly-notes-14-2026/">week note</a>!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>I guess I cook now</title>
      <link>https://msfjarvis.dev/posts/i-guess-i-cook-now/</link>
      <pubDate>Wed, 01 Apr 2026 14:56:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/i-guess-i-cook-now/</guid>
      <description>Learning to cook for myself went from necessity to hobby almost overnight</description>
      <content:encoded><![CDATA[<blockquote>
<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>
</blockquote>
<p>For the longest time I&rsquo;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>
<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>
<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 &ldquo;Arbi&rdquo; in Hindi) based curry, a favorite of Yash&rsquo;, 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&rsquo;re supposed to thaw chicken 6 hours in advance (it was edible).</p>
<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&rsquo;t pounding McChickens three times a day. My photo gallery in the past year is probably 40% dinner plates by count. I&rsquo;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>
<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&rsquo;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&rsquo;ll see the light.</p>
<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&rsquo;s week to try it out and ended up with a genuinely delicious batch.</p>
<figure>
    <img loading="lazy" src="day-1-hummus.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."/> <figcaption>
            This was a bit of an arm workout not gonna lie
        </figcaption>
</figure>

<figure>
    <img loading="lazy" src="day-1-hummus-result.webp"
         alt="A small metal plate with some hummus that I&#39;ve done a poor job of garnishing with some olive oil, red chilli powder and cumin powder."/> <figcaption>
            4/10 plating but 8/10 flavour, I&#39;ll take it.
        </figcaption>
</figure>

<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&rsquo;t nailed that one, but got very close the last time!</p>
<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>
<p><figure>
    <img loading="lazy" src="the-birth-of-a-burrito.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."/> <figcaption>
            The internet told me this is how you make a burrito with two wraps.
        </figcaption>
</figure>
<figure>
    <img loading="lazy" src="burrito-pan.webp"
         alt="A burrito being lightly grilled on a pan. Shocking it has not split open its guts all over the place."/> <figcaption>
            One of the rare few burritos that stayed in one piece when we grilled them.
        </figcaption>
</figure>
</p>
<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&rsquo;m feeling lazy, has helped immensely with my struggle to lose weight.</p>
<hr />

<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&rsquo;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&rsquo;ll pick up this year!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Weeknotes: Week #13 (2026)</title>
      <link>https://msfjarvis.dev/posts/weeknotes-week-13-2026/</link>
      <pubDate>Sun, 29 Mar 2026 20:12:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/weeknotes-week-13-2026/</guid>
      <description>A Flu is no joke&amp;hellip;</description>
      <content:encoded><![CDATA[<p>This week has been a bit of a roller coaster, was having a pretty normal week then got hit with a <em>nasty</em> Flu to cap it off in the worst way possible.</p>
<h3 id="boring-tech-stuff">Boring tech stuff</h3>
<p>I&rsquo;ve been trying out <a href="https://github.com/obra/superpowers">superpowers</a>, which takes gaslighting-via-markdown (otherwise known as Agent Skills) to a logical extreme, forcing LLMs to behave like a very disciplined senior engineer.</p>
<p>I used this to change a longstanding misfeature in Claw that caused it to mark comments as unread when you first view a post, which is quite redundant. superpowers spent a solid hour on this, during which I also manually reviewed a spec and an implementation plan, but the <a href="https://github.com/msfjarvis/compose-lobsters/commit/83db9bb8a8f7f1ae7dc3df305ab599f15fa7d512">result</a> is quite exhaustive and has robust test coverage which I definitely wouldn&rsquo;t have bothered with.</p>
<p>I also let it loose on our work codebase by feeding it a list of prior issues we had faced in a particularly brittle component and had it refactor the code to follow the popular <a href="https://testing.googleblog.com/2025/10/simplify-your-code-functional-core.html">&ldquo;functional core, imperative shell&rdquo;</a> pattern and then unit test the core to prevent regressions along the bug categories we&rsquo;ve been chasing for quite a while. That refactor ran even longer, but ultimately the result was again rather impressive. LLMs are (for the most part) really good at fixing tests, and superpowers forces the LLM to follow a red-green-refactor cycle for all changes which means it&rsquo;s always trying to fix tests.</p>
<p>I finally finished running the full gauntlet of <a href="https://webmention.rocks">webmention.rocks</a> tests against my <a href="https://git.msfjarvis.dev/msfjarvis/acceptable-vibes/src/branch/main/webmentions-server">WebMentions server</a>. It exposed a few gaps in the implementation but I&rsquo;m at 100% compliance now, the only remaining issue is now with my site not returning HTTP 410 for deleted posts because I don&rsquo;t really know how to make it happen.</p>
<h3 id="boring-not-tech-stuff">Boring not-tech stuff</h3>
<p>I finished reading <a href="https://bookwyrm.social/book/102060/s/project-hail-mary">Project Hail Mary</a>, super good read. I hadn&rsquo;t realized Andy Weir also wrote The Martian! The book was great, the movie&rsquo;s promised to be even better but good lord has it been hard to find a way to watch it with the lack of IMAX screens.</p>
<p>The keyboard I had ordered last week arrived, completing the new set :)</p>
<figure>
    <img loading="lazy" src="keyboard-and-mouse.webp"
         alt="The new keyboard and mouse on top of a wooden particle board desk. The keyboard has an aluminum frame coated in blue, with a mix of blue, purple and white keys with white being the most common one. The mouse rests on a cheap RedGear mousepad."/> <figcaption>
            The white mouse honestly works better than the lavender
        </figcaption>
</figure>

<p>I hadn&rsquo;t spotted our Metro station cat in a while, so it was nice to come across it again.</p>
<figure>
    <img loading="lazy" src="orange-loaf.webp"
         alt="A ginger cat loafing in the middle of two bar stools."/> <figcaption>
            Angy loaf
        </figcaption>
</figure>

<h3 id="health-woes">Health woes</h3>
<p>I caught a fever on Thursday at the office which was starting to feel concerning right away, and by night had gotten bad enough that I had to arrange to see a doctor in the morning. Got diagnosed with a Flu and got a big dose of medicines to work through the weekend and see the doctor again on Monday. Was not expecting the throat problems, even my damn uvula had swollen and I could barely drink or breath. Super rough 3 days so far, the fever&rsquo;s settled down now but I still have a very sore mouth and have trouble eating and swallowing. <em>Pain</em></p>
<h3 id="and-finally-the-food">And finally, the food</h3>
<p>Before I got absolutely mauled by the Flu I did do some cooking, which mostly was burrito prep that lasted us two days.</p>
<figure>
    <img loading="lazy" src="burrito-bowl-v1.webp"
         alt="A typical burrito bowl with rice, corn, lettuce, beans, diced jalapenos and some condiments."/> <figcaption>
            I feel burrito bowls are kind of underrated
        </figcaption>
</figure>

<p>I had some chicken breasts sitting in the freezer that I wanted to finish off, so I used some of the rice from the large batch we made for the burrito bowl meal prep and made a basic tomato and cream curry for the chicken which came out pretty delicious for being basically 20 minutes of work</p>
<figure>
    <img loading="lazy" src="chicken-curry.webp"
         alt="A plate of two chicken breasts and some rice with gravy over it. The gravy is reddish-orange in color due to the tomatoes, turmeric and heavy cream coming together. There are also bits of onion visible in the gravy."/> <figcaption>
            Scrumptious and filling. This cooking shit is easy :P
        </figcaption>
</figure>

<hr />

<p>Hopefully the next weeknote will find me healthier :(</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Weeknotes: Week #12 (2026)</title>
      <link>https://msfjarvis.dev/posts/weeknotes-week-12-2026/</link>
      <pubDate>Sun, 22 Mar 2026 16:02:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/weeknotes-week-12-2026/</guid>
      <description>Movies, food, rain, and an early birthday present to myself!</description>
      <content:encoded><![CDATA[<h2 id="life">Life</h2>
<p>As I mentioned <a href="/posts/weeknotes-week-11-2026/">last week</a>, I watched <a href="https://www.imdb.com/title/tt27564844/">Iron Lung</a> on Monday. Only the premium cinemas were running shows so I had to buy an expensive ticket at PVR Director&rsquo;s Cut, and got to watch it in a reclining chair that was basically a bed. Loved the movie, Markiplier did great work as both director and actor. Hopefully we can get a couple more of these authentic video game recreations from individual creators before the suits at Hollywood figure out how to ruin them even more.</p>
<figure>
    <img loading="lazy" src="iron-lung.webp"
         alt="A scene from the movie Iron Lung, showing Markiplier inside a submarine. The subtitle reads &#34;I&#39;m seeing some voltage irregularities&#34;."/> <figcaption>
            Iron Lung was a great watch
        </figcaption>
</figure>

<p>I was back in the office this week, work was mostly CI and suffering. On Wednesday I got caught out in the hailstorm on my way back from the office. Thankfully I didn&rsquo;t catch anything from that, but I did have to figure out how to wash my shoes which was surprisingly easy — thanks Skechers.</p>
<figure>
    <img loading="lazy" src="rain-and-hail.webp"
         alt="A short video of rain and hail falling on a slightly waterlogged asphalt road. A red Maruti Suzuki Zen car is visible in the top left corner, with a partial number plate."/> <figcaption>
            It was 30 degree Celsius at noon, and by 6 PM I was getting pelted by hail!
        </figcaption>
</figure>

<h2 id="toys">Toys</h2>
<p>I had been saving up to gift myself a new keyboard and mouse for my birthday, and ended up making it an early gift by placing the orders this week. I went with the <a href="https://www.genesispc.in/products/wobkey-rainy75-mechanical-keyboard?variant=43852369068085">Wobkey Rainey75</a> keyboard and a <a href="https://meckeys.com/shop/mouse/vxe-r1-series/?attribute_pa_variations=r1-pro-max&amp;attribute_pa_colour-style=white">VXE R1 Pro Max</a> mouse. The mouse arrived today, surprisingly in white rather than the purple/lavender I thought I had chosen. Clearly I made a mistake during the order, but I&rsquo;m quite happy with it regardless. It&rsquo;s been a long time since I had a wireless mouse and it&rsquo;s such a massive difference from my wired <a href="https://www.razer.com/gaming-mice/razer-viper-mini">Razer Viper Mini</a>. Also nice to have a working scroll wheel again :')</p>
<p>I also bought myself a microphone arm so I could get a bit more desk space, and struggled through a few bouts of stupidity before realizing that I needed an additional screw to plug my Blue Yeti into the standard 3/8 inch mounts that feature in the microphone arms. I ordered said screw from Amazon, set it all up today and when I went to put the old desk stand in the box I realized that the screw actually came with the microphone and I am just a one-of-a-kind dumbass.</p>
<p>I had been bracing for pricing changes for my Netcup VPS, and it <a href="https://www.netcup.com/en/priceadjustment">finally arrived</a> this week. I run a pretty small ARM VPS at Netcup so my costs are only going from 5.84 EUR to 6.92 EUR, a hit I can take for the time being. Really unfortunate that the unprofitable companies propping up the AI bubble have so much access to free capital that they&rsquo;re destabilizing the whole industry.</p>
<h2 id="books">Books</h2>
<p>I installed <a href="https://github.com/janeczku/calibre-web/">Calibre-Web</a> last week, and spent most of this week doing manual housekeeping on the 40 or so books I own. Cleaning up metadata, syncing read states, setting up shelves — a lot of frankly pointless work that will hopefully make life easier in the long run. At IndieWebClub I tried to give <a href="https://tanvibhakta.in">Tanvi</a> her own account on my instance, but hadn&rsquo;t realized that we&rsquo;d have to share the library. I&rsquo;ve set up a shelf for my books so we can hopefully rely on that to work around the problem.</p>
<p>With my reading goal quite lagging, I finally finished off <a href="https://bookwyrm.social/book/235804/s/the-wizard-hunters-the-fall-of-ile-rien-book-1">The Wizard Hunters</a> that I started <a href="https://bookwyrm.social/user/msfjarvis/generatednote/9824676">reading on February 6</a>. I usually get through a book in 2 to 3 weeks, but this was slow both because it was technically two books in one and hence around 700 pages in length, and because I was just not reading as often the past few weeks. I want to read <a href="https://bookwyrm.social/book/102060/s/project-hail-mary">Project Hail Mary</a> before I watch the movie, so hopefully reading this in the next week will put me back on track for my goal of reading 24 books a year.</p>
<h2 id="writing">Writing</h2>
<p>At IndieWebClub this week I finished off and published the post about securing <a href="https://msfjarvis.dev/posts/setting-up-forward-auth-with-caddy-and-pocket-id/">Calibre-Web behind Caddy and Pocket ID</a>.</p>
<h2 id="and-finally-the-food">And finally, the food</h2>
<p>As I promised last week, with the missing Cloudflare R2 integration in my CMS no longer being an excuse for not showing off the food I keep talking about, here is some of the food I made this week.</p>
<p>I had some chicken breast lying around in the freezer and an overwhelming desire to consume it, so I threw together a pretty simple pasta sauce using tomato and cream, cooked the chicken in it and then tossed it with pasta and some spices. I used the air fryer to quickly toast some sourdough bread I had and that was dinner and the next day&rsquo;s breakfast.</p>
<figure>
    <img loading="lazy" src="chicken-pasta.webp"
         alt="A bowl of Fusilli pasta in a creamy and tomato rich sauce with some chicken pieces and a couple of slices of sourdough bread poking out from it."/> <figcaption>
            Behold, the pasta.
        </figcaption>
</figure>

<p>Another night I ordered some nachos with my groceries so I put together a simple salad to go with the nachos. Apparently this is called <a href="https://www.spendwithpennies.com/cowboy-caviar/">&ldquo;cowboy caviar&rdquo;</a> in the US of A. I made this on a whim so I didn&rsquo;t have kidney beans, but I did get some feta cheese and it came out very nice. Bright flavors, super satiating, lots of fiber. What&rsquo;s not to like?</p>
<figure>
    <img loading="lazy" src="cowboy-caviar.webp"
         alt="The cowboy caviar salad containing diced onions, cucumbers, cherry tomatoes, corn, feta cheese and a simple sauce made with olive oil, apple cider vinegar, lemon juice and onion powder."/> <figcaption>
            Breakfast of champions or something I don&#39;t know.
        </figcaption>
</figure>

<p><a href="https://yashgarg.dev/">Yash</a> was back home on Saturday so we went back to our classic rajma (kidney beans) gravy which I supplemented with a side of raita.</p>
<figure>
    <img loading="lazy" src="rajma-and-raita.webp"
         alt="A plate with a bowl of raita, a bowl of rajma gravy and two chapattis."/> <figcaption>
            Feels like home.
        </figcaption>
</figure>

]]></content:encoded>
    </item>
    
    <item>
      <title>Setting up forward auth with Caddy and Pocket ID</title>
      <link>https://msfjarvis.dev/posts/setting-up-forward-auth-with-caddy-and-pocket-id/</link>
      <pubDate>Sat, 21 Mar 2026 15:33:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/setting-up-forward-auth-with-caddy-and-pocket-id/</guid>
      <description>Using Pocket ID to secure services being proxied by Caddy</description>
      <content:encoded><![CDATA[<p>As I mentioned in my <a href="/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&rsquo;m documenting it here in hopes of helping others.</p>
<h2 id="requirements">Requirements</h2>
<ul>
<li><a href="https://caddyserver.com/">Caddy</a> with the <a href="https://github.com/greenpau/caddy-security/">caddy-security</a> plugin</li>
<li><a href="https://pocket-id.org/">Pocket ID</a></li>
<li>Patience.</li>
</ul>
<h2 id="pocket-id-and-caddy-security-setup">Pocket ID and caddy-security setup</h2>
<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&rsquo;ll need to make some tweaks.</p>
<p>First of all, in the <code>oauth identity provider</code> block, add this line:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl">trust login redirect uri domain exact ${app.domain} path prefix /
</span></span></code></pre></div><p>Replace <code>${app.domain}</code> with the domain to the service you are securing.</p>
<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&rsquo;ll explain the basic changes first then dive into the NixOS-specific stuff I did for my own deployment.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl">security {
</span></span><span class="line"><span class="cl">  # Rename the provider from generic to the service name
</span></span><span class="line"><span class="cl">  oauth identity provider calibreweb {
</span></span><span class="line"><span class="cl">    delay_start 3
</span></span><span class="line"><span class="cl">    # Give this its own realm
</span></span><span class="line"><span class="cl">    realm calibreweb
</span></span><span class="line"><span class="cl">    # This is the OIDC provider type, make sure this stays `generic`
</span></span><span class="line"><span class="cl">    driver generic
</span></span><span class="line"><span class="cl">    # Service-scoped secrets from the Pocket ID OIDC client setup
</span></span><span class="line"><span class="cl">    client_id {$CALIBRE_WEB_POCKET_ID_CLIENT_ID}
</span></span><span class="line"><span class="cl">    client_secret {$CALIBRE_WEB_POCKET_ID_CLIENT_SECRET}
</span></span><span class="line"><span class="cl">    scopes openid email profile
</span></span><span class="line"><span class="cl">    base_auth_url https://auth.msfjarvis.dev
</span></span><span class="line"><span class="cl">    metadata_url https://auth.msfjarvis.dev/.well-known/openid-configuration
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">   # Rename the authentication portal as well
</span></span><span class="line"><span class="cl">  authentication portal calibreweb_portal {
</span></span><span class="line"><span class="cl">    crypto default token lifetime 3600
</span></span><span class="line"><span class="cl">    # Use the new name for the identity provider defined above
</span></span><span class="line"><span class="cl">    enable identity provider calibreweb
</span></span><span class="line"><span class="cl">    trust login redirect uri domain exact books.msfjarvis.dev path prefix /
</span></span><span class="line"><span class="cl">    cookie insecure off
</span></span><span class="line"><span class="cl">    cookie domain books.msfjarvis.dev
</span></span><span class="line"><span class="cl">    transform user {
</span></span><span class="line"><span class="cl">      # The new realm name
</span></span><span class="line"><span class="cl">      match realm calibreweb
</span></span><span class="line"><span class="cl">      action add role user
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  # Per-service configuration
</span></span><span class="line"><span class="cl">  authorization policy calibreweb_policy {
</span></span><span class="line"><span class="cl">    set auth url /caddy-security/oauth2/calibreweb
</span></span><span class="line"><span class="cl">    allow roles user
</span></span><span class="line"><span class="cl">    inject headers with claims
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><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>
<h3 id="sidebar-nixos-version">Sidebar: NixOS version</h3>
<blockquote>
<p>People not using NixOS can ignore this section</p>
</blockquote>
<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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="n">pocketIdApplications</span> <span class="o">=</span> <span class="n">mkOption</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">type</span> <span class="o">=</span> <span class="n">types</span><span class="o">.</span><span class="n">attrsOf</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">types</span><span class="o">.</span><span class="n">submodule</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">options</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">domain</span> <span class="o">=</span> <span class="n">mkOption</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="n">type</span> <span class="o">=</span> <span class="n">types</span><span class="o">.</span><span class="n">str</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">          <span class="n">description</span> <span class="o">=</span> <span class="s2">&#34;Domain of the proxied service&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">};</span>
</span></span><span class="line"><span class="cl">        <span class="n">clientIdEnvVar</span> <span class="o">=</span> <span class="n">mkOption</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="n">type</span> <span class="o">=</span> <span class="n">types</span><span class="o">.</span><span class="n">str</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">          <span class="n">description</span> <span class="o">=</span> <span class="s2">&#34;Environment variable name containing the client ID&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">};</span>
</span></span><span class="line"><span class="cl">        <span class="n">clientSecretEnvVar</span> <span class="o">=</span> <span class="n">mkOption</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="n">type</span> <span class="o">=</span> <span class="n">types</span><span class="o">.</span><span class="n">str</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">          <span class="n">description</span> <span class="o">=</span> <span class="s2">&#34;Environment variable name containing the client secret&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">};</span>
</span></span><span class="line"><span class="cl">      <span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="n">default</span> <span class="o">=</span> <span class="p">{</span> <span class="p">};</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="o">=</span> <span class="s2">&#34;Applications to protect with Pocket ID OIDC via caddy-security&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span></code></pre></div><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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="err">$</span><span class="p">{</span><span class="n">lib</span><span class="o">.</span><span class="n">optionalString</span> <span class="p">(</span><span class="n">config</span><span class="o">.</span><span class="n">services</span><span class="o">.</span><span class="n">caddy</span><span class="o">.</span><span class="n">pocketIdApplications</span> <span class="o">!=</span> <span class="p">{</span> <span class="p">})</span> <span class="s1">&#39;&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">  security {
</span></span></span><span class="line"><span class="cl"><span class="s1">    </span><span class="si">${</span><span class="n">lib</span><span class="o">.</span><span class="n">concatStringsSep</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">lib</span><span class="o">.</span><span class="n">mapAttrsToList</span> <span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="n">app</span><span class="p">:</span> <span class="s1">&#39;&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">        oauth identity provider </span><span class="si">${</span><span class="n">name</span><span class="si">}</span><span class="s1"> {
</span></span></span><span class="line"><span class="cl"><span class="s1">          delay_start 3
</span></span></span><span class="line"><span class="cl"><span class="s1">          realm </span><span class="si">${</span><span class="n">name</span><span class="si">}</span><span class="s1">
</span></span></span><span class="line"><span class="cl"><span class="s1">          driver generic
</span></span></span><span class="line"><span class="cl"><span class="s1">          client_id {</span><span class="si">${</span><span class="n">app</span><span class="o">.</span><span class="n">clientIdEnvVar</span><span class="si">}</span><span class="s1">}
</span></span></span><span class="line"><span class="cl"><span class="s1">          client_secret {</span><span class="si">${</span><span class="n">app</span><span class="o">.</span><span class="n">clientSecretEnvVar</span><span class="si">}</span><span class="s1">}
</span></span></span><span class="line"><span class="cl"><span class="s1">          scopes openid email profile
</span></span></span><span class="line"><span class="cl"><span class="s1">          base_auth_url https://auth.msfjarvis.dev
</span></span></span><span class="line"><span class="cl"><span class="s1">          metadata_url https://auth.msfjarvis.dev/.well-known/openid-configuration
</span></span></span><span class="line"><span class="cl"><span class="s1">        }
</span></span></span><span class="line"><span class="cl"><span class="s1">
</span></span></span><span class="line"><span class="cl"><span class="s1">        authentication portal </span><span class="si">${</span><span class="n">name</span><span class="si">}</span><span class="s1">_portal {
</span></span></span><span class="line"><span class="cl"><span class="s1">          crypto default token lifetime 3600
</span></span></span><span class="line"><span class="cl"><span class="s1">          enable identity provider </span><span class="si">${</span><span class="n">name</span><span class="si">}</span><span class="s1">
</span></span></span><span class="line"><span class="cl"><span class="s1">          trust login redirect uri domain exact </span><span class="si">${</span><span class="n">app</span><span class="o">.</span><span class="n">domain</span><span class="si">}</span><span class="s1"> path prefix /
</span></span></span><span class="line"><span class="cl"><span class="s1">          cookie insecure off
</span></span></span><span class="line"><span class="cl"><span class="s1">          cookie domain </span><span class="si">${</span><span class="n">app</span><span class="o">.</span><span class="n">domain</span><span class="si">}</span><span class="s1">
</span></span></span><span class="line"><span class="cl"><span class="s1">          transform user {
</span></span></span><span class="line"><span class="cl"><span class="s1">            match realm </span><span class="si">${</span><span class="n">name</span><span class="si">}</span><span class="s1">
</span></span></span><span class="line"><span class="cl"><span class="s1">            action add role user
</span></span></span><span class="line"><span class="cl"><span class="s1">          }
</span></span></span><span class="line"><span class="cl"><span class="s1">        }
</span></span></span><span class="line"><span class="cl"><span class="s1">
</span></span></span><span class="line"><span class="cl"><span class="s1">        authorization policy </span><span class="si">${</span><span class="n">name</span><span class="si">}</span><span class="s1">_policy {
</span></span></span><span class="line"><span class="cl"><span class="s1">          set auth url /caddy-security/oauth2/</span><span class="si">${</span><span class="n">name</span><span class="si">}</span><span class="s1">
</span></span></span><span class="line"><span class="cl"><span class="s1">          allow roles user
</span></span></span><span class="line"><span class="cl"><span class="s1">          inject headers with claims
</span></span></span><span class="line"><span class="cl"><span class="s1">        }
</span></span></span><span class="line"><span class="cl"><span class="s1">      &#39;&#39;</span><span class="p">)</span> <span class="n">config</span><span class="o">.</span><span class="n">services</span><span class="o">.</span><span class="n">caddy</span><span class="o">.</span><span class="n">pocketIdApplications</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span><span class="si">}</span><span class="s1">
</span></span></span><span class="line"><span class="cl"><span class="s1">  }
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;&#39;</span><span class="p">}</span>
</span></span></code></pre></div><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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="n">sops</span><span class="o">.</span><span class="n">secrets</span><span class="o">.</span><span class="n">calibre-web-caddy-env</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">sopsFile</span> <span class="o">=</span> <span class="n">lib</span><span class="o">.</span><span class="n">snowfall</span><span class="o">.</span><span class="n">fs</span><span class="o">.</span><span class="n">get-file</span> <span class="s2">&#34;secrets/calibre-web.env&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">format</span> <span class="o">=</span> <span class="s2">&#34;dotenv&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">owner</span> <span class="o">=</span> <span class="n">config</span><span class="o">.</span><span class="n">services</span><span class="o">.</span><span class="n">caddy</span><span class="o">.</span><span class="n">user</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">restartUnits</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">&#34;caddy.service&#34;</span> <span class="p">];</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="n">services</span><span class="o">.</span><span class="n">caddy</span><span class="o">.</span><span class="n">pocketIdApplications</span><span class="o">.</span><span class="s2">&#34;calibreweb&#34;</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">domain</span> <span class="o">=</span> <span class="n">cfg</span><span class="o">.</span><span class="n">domain</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">clientIdEnvVar</span> <span class="o">=</span> <span class="s2">&#34;$CALIBRE_WEB_POCKET_ID_CLIENT_ID&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">clientSecretEnvVar</span> <span class="o">=</span> <span class="s2">&#34;$CALIBRE_WEB_POCKET_ID_CLIENT_SECRET&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="n">systemd</span><span class="o">.</span><span class="n">services</span><span class="o">.</span><span class="n">caddy</span><span class="o">.</span><span class="n">serviceConfig</span><span class="o">.</span><span class="n">EnvironmentFile</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">  <span class="n">config</span><span class="o">.</span><span class="n">sops</span><span class="o">.</span><span class="n">secrets</span><span class="o">.</span><span class="n">calibre-web-caddy-env</span><span class="o">.</span><span class="n">path</span>
</span></span><span class="line"><span class="cl"><span class="p">];</span>
</span></span></code></pre></div><h2 id="caddy-routing-setup">Caddy routing setup</h2>
<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&rsquo;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>
<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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl">handle /caddy-security/* {
</span></span><span class="line"><span class="cl">  route {
</span></span><span class="line"><span class="cl">    authenticate with calibreweb_portal
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><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&rsquo;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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl">@integrations {
</span></span><span class="line"><span class="cl">  path /opds /opds/* /kobo /kobo/*
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">handle @integrations {
</span></span><span class="line"><span class="cl">  reverse_proxy localhost:8080 {
</span></span><span class="line"><span class="cl">    header_up X-Scheme https
</span></span><span class="line"><span class="cl">    transport http {
</span></span><span class="line"><span class="cl">      read_buffer 1024k
</span></span><span class="line"><span class="cl">      write_buffer 1024k
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><p>This is the catch-all route for everything else that doesn&rsquo;t need special treatment. This only differs from the one above in requiring <em>authorization</em>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl">handle {
</span></span><span class="line"><span class="cl">  route {
</span></span><span class="line"><span class="cl">    authorize with calibreweb_policy
</span></span><span class="line"><span class="cl">    reverse_proxy localhost:8080 {
</span></span><span class="line"><span class="cl">      header_up X-Scheme https
</span></span><span class="line"><span class="cl">      transport http {
</span></span><span class="line"><span class="cl">        read_buffer 1024k
</span></span><span class="line"><span class="cl">        write_buffer 1024k
</span></span><span class="line"><span class="cl">      }
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><p>The full configuration then becomes this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl">handle /caddy-security/* {
</span></span><span class="line"><span class="cl">  route {
</span></span><span class="line"><span class="cl">    authenticate with calibreweb_portal
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">@integrations {
</span></span><span class="line"><span class="cl">  path /opds /opds/* /kobo /kobo/*
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">handle @integrations {
</span></span><span class="line"><span class="cl">  reverse_proxy localhost:8080 {
</span></span><span class="line"><span class="cl">    header_up X-Scheme https
</span></span><span class="line"><span class="cl">    transport http {
</span></span><span class="line"><span class="cl">      read_buffer 1024k
</span></span><span class="line"><span class="cl">      write_buffer 1024k
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">handle {
</span></span><span class="line"><span class="cl">  route {
</span></span><span class="line"><span class="cl">    authorize with calibreweb_policy
</span></span><span class="line"><span class="cl">    reverse_proxy localhost:8080 {
</span></span><span class="line"><span class="cl">      header_up X-Scheme https
</span></span><span class="line"><span class="cl">      transport http {
</span></span><span class="line"><span class="cl">        read_buffer 1024k
</span></span><span class="line"><span class="cl">        write_buffer 1024k
</span></span><span class="line"><span class="cl">      }
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><h3 id="sidebar-authentication-vs-authorization">Sidebar: Authentication vs Authorization</h3>
<p>This is something that took too long for me to hammer into my brain, so I&rsquo;ll reproduce it here as well for others who may have had the same question but didn&rsquo;t feel confident asking it.</p>
<ul>
<li>Authentication: Verifying your identity. This answers the question of who the user is.</li>
<li>Authorization: Verifying your access. This answers the question of whether the previously identified user should be allowed through to a specific service.</li>
</ul>
<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>
<hr />

<p>And that&rsquo;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&rsquo;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>
]]></content:encoded>
    </item>
    
    <item>
      <title>Weeknotes: Week #11 (2026)</title>
      <link>https://msfjarvis.dev/posts/weeknotes-week-11-2026/</link>
      <pubDate>Sun, 15 Mar 2026 19:37:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/weeknotes-week-11-2026/</guid>
      <description>Would you believe it, even more travel!</description>
      <content:encoded><![CDATA[<p>I am starting to realize that every other week I talk about traveling somewhere, in the midst of which I also threw in the part about being somewhat lonely and depressed when I had to be by myself in one place for a couple of weeks. This is probably giving people the impression that I love to travel and despise being a homebody, which is quite literally the exact opposite of who I really am. Hopefully this week was my last trip for the next few months and I can reclaim my nerdy homebody persona.</p>
<p>As hinted above, I was back home this week! My friend Rohan was getting married and I couldn&rsquo;t imagine not being there, especially when our whole friend group was getting together. It was a great time, he sounded genuinely excited and happy which is all I care for. I got to see a lot of friends I haven&rsquo;t met in person for over a year now, and it was very nice catching up and being a little rowdy over some drinks :)</p>
<p>While at home I also took the opportunity to hack some more on the homelab server and try out <a href="https://microvm-nix.github.io/microvm.nix/">microvm.nix</a> to enable running more than one instance of things on the same machine which has historically been a problem on NixOS. I intended to use all that preparatory work to try out <a href="https://github.com/booklore-app/booklore">Booklore</a> in a microVM since it uses MariaDB which I had no plans to run on my host, but that fell through when my suspicions around the quality issues in the v2 release were <a href="https://www.reddit.com/r/selfhosted/comments/1rs275q/psa_think_hard_before_you_deploy_booklore/">validated</a> by the revelation that the developer has let Claude run rampant over what was honestly a pretty decent bit of software. The following <a href="https://www.reddit.com/r/selfhosted/comments/1rs4nx0/my_side_of_the_story_from_the_developer_of/">meltdown from the developer</a> cemented my decision to stay away from it entirely. Instead, I set myself up with <a href="https://github.com/janeczku/calibre-web">Calibre-Web</a>.</p>
<p>It was quite straightforward, and most of my time tinkering with it actually went into setting up forward authentication using Caddy and Pocket ID. The end result of it can be found <a href="https://github.com/msfjarvis/dotfiles/blob/d4d81b24cd37456194c2e1a7ca8dde90fa19a986/modules/nixos/caddy/default.nix#L72-L107">here</a>, and I intend to write a post about it next week so others have to suffer a little less than I had to :')</p>
<p>At home, I do my gaming on Linux where I ran into a peculiar bug involving Discord and Tailscale, which didn&rsquo;t let me join any voice calls. I investigated this a bit online, and apparently it was <a href="github.com/tailscale/tailscale/issues/10396">reported already</a> and the <a href="github.com/tailscale/tailscale/issues/10396#issuecomment-3871203280">fix</a> was to hide the <code>tailscale0</code> interface from the Discord client?? That was also how I found about <a href="https://github.com/netblue30/firejail">firejail</a>, and like most things there was already a NixOS module <a href="https://github.com/msfjarvis/dotfiles/commit/d2ea971bd1276e1f47510711a7c5e813c24a50f9">to make the setup easy</a>.</p>
<p>On Saturday I flew back home via a morning flight, which is weirdly the first time I realized that the flight between Bengaluru and Hindon is closer to 3 hours than 2. Always thought otherwise, don&rsquo;t know why.</p>
<p>I found out about <a href="https://izzyondroid.org/docs/general/AppInclusionPolicy/#ai-policy">IzzyOnDroid&rsquo;s AI policy</a> yesterday and since I&rsquo;ve <a href="/posts/coming-around-on-the-utility-of-llms/">written before</a> about using LLMs for the development of Claw, I filed <a href="https://codeberg.org/IzzyOnDroid/repodata/issues/60">an issue</a> to ask for its removal from the repository. I massively appreciate the work they do for the FOSS ecosystem for Android (having donated a not-insignificant amount to their OpenCollective through Android Password Store) and I would hate to take that for granted and knowingly ignore their policies. There hasn&rsquo;t been a decision made yet on it, but I&rsquo;ve provided all the necessary context to aid the process.</p>
<p><a href="https://github.com/swapnilmadavi">Swapnil</a> and I went to watch <a href="https://www.imdb.com/title/tt33035197">Boong</a> right before I sat down to write this weeknote. Very cute and heartwarming, really loved it. It also made me realize how much I&rsquo;ve missed going to the movies this past month, so I&rsquo;m remedying it by watching <a href="https://www.imdb.com/title/tt27564844/">Iron Lung</a> tomorrow.</p>
<p>More meta stuff: I&rsquo;ve <a href="/posts/weeknotes-week-8-2026/">previously talked</a> about how my CMS does not have an integration with Cloudflare R2 as an excuse to not attach pictures of my food, which obviously meant that <a href="https://github.com/sveltia/sveltia-cms/commit/ff7aa3d885b4733d87c64c3a4fad350e325c6c99">the feature got shipped</a> so I have no reason to be lazy anymore. Prepare for diabolical salads next week!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Weeknotes: Week #10 (2026)</title>
      <link>https://msfjarvis.dev/posts/weeknotes-week-10-2026/</link>
      <pubDate>Sun, 08 Mar 2026 22:07:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/weeknotes-week-10-2026/</guid>
      <description>The most boring week I&amp;rsquo;ve had this year</description>
      <content:encoded><![CDATA[<p>Barely anything of note happened this week as practically my whole team was out-of-office so I was both working <em>and</em> living alone which made for an incredibly boring time.</p>
<p>My phone woes continued into this week, with the Pixel 8 seemingly forgetting my home Wi-Fi network entirely and refusing to connect to it. For two days I had it connect to my spare phone&rsquo;s hotspot for Wi-Fi. Just like last week, this problem also magically resolved itself in two days and things went back to normal. I hate software even more.</p>
<p>I finally had to go bug my landlord to get some maintenance work done in the flat when my ceiling fan broke down, not a fun time in 32 degree Celsius weather. That took entirely too many phone calls than one would expect, but I managed to take the opportunity and get other stuff fixed around the apartment as well. Hopefully that&rsquo;ll be the end of this.</p>
<p>At work I kept fighting with CI related things, which were made slower by the fact that I had to reach out to a different team to help with reviews since nobody in mine was working. Eventually I think I&rsquo;ve got it all working but what a colossal waste of time it has been throughout this week. We had a <a href="https://www.cloudflarestatus.com/incidents/cs9vyjjj4trx">minor scare</a> with the Android SDK from a user report about crashes when attempting to join a meeting but thankfully we were able to debug it and identify that the problem wasn&rsquo;t as widespread as we initially suspected and required a very specific setup to trigger. I fixed the root cause and cut a release, I&rsquo;ll probably write a blog post about it once I&rsquo;ve completed the internal incident report.</p>
<p>I had to fly back home via an early flight on the 8th and hadn&rsquo;t packed for it yet so I was debating if I should go to IndieWebClub but thankfully I did end up going, the <a href="https://underline.center/t/indiewebclub-21/723">topics on the agenda</a> were great and a lot of thoughtful discussion happened that I enjoyed participating in.</p>
<p>I packed for the trip at a definitely-not-too-late 8 PM, stayed up till 2 AM playing games with friends while waiting for my taxi and managed to reach the airport in an astonishing 45 minutes. All the airport staff was still there but the foot traffic was maybe a fifth of what I usually see in Kempegowda airport, so I basically speedran through check-in and security. Time from taxi to departure gate, maybe 30 minutes.</p>
<p>And that was kind of it! I&rsquo;m back home in Ghaziabad for a week to attend a friend&rsquo;s wedding and will be back in Bengaluru next weekend.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Weeknotes: Week #9 (2026)</title>
      <link>https://msfjarvis.dev/posts/weeknotes-week-9-2026/</link>
      <pubDate>Sun, 01 Mar 2026 19:37:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/weeknotes-week-9-2026/</guid>
      <description>Getting a little honest about my mental health</description>
      <content:encoded><![CDATA[<blockquote>
<p>Warning: Feelings</p>
</blockquote>
<p>My roommate <a href="https://yashgarg.dev/">Yash</a> is back with his parents for a couple of weeks, which has left me alone in the house and quite severely exposed my delusion of being a loner. I&rsquo;ll probably write a separate post about this at some point, but I&rsquo;ve been quite overwhelmed with feelings of loneliness which aren&rsquo;t helped at all by me being a general homebody so this week has been quite rough, and forced me to start training myself to address the things that feed into it. I ended up finally utilizing the free therapy options offered at Cloudflare, and make sure to have at least one cleaning related task on my daily to-do to ensure I do not spiral into my room becoming a depression nest (shoutout <a href="https://www.youtube.com/watch?v=WjsX_BfhRSo">Geega</a> my goat). This has helped so far, and will likely be plenty to tide me over until I myself go back to my family at the end of next week. I also reached out to my colleagues at work and have scheduled a day of working together out of a cafe since our teams agreed to work from home for the next week due to most people taking off for Holi.</p>
<p>I promise I&rsquo;ll be okay, it&rsquo;s a bit of a sad boy funk, I&rsquo;ll get over it.</p>
<hr />

<p>Onward to the rest of the weeknote!</p>
<ul>
<li>I had to email the customer support for one my credit cards when I realized I needed to use my registered email, and had <em>helpfully</em> used one of DuckDuckGo&rsquo;s disposable aliases for the job 😅. Figuring that one out was thankfully pretty quick, and has been <a href="https://msfjarvis.dev/notes/til-sending-emails-from-my-duckduckgo-email-alias/">put down in a note</a> for future Harsh.</li>
<li><a href="https://kat.bio/">Amogh</a> helped me figure out how to be able to pay for stuff on Steam (the aforementioned credit card being at fault) again, so any irresponsible gaming purchases I talk about in the coming weeks are his fault.</li>
<li><a href="/posts/weeknotes-week-8-2026/">Last week</a> I talked about adding WebMentions support to this site, this week I kept iterating on it and it now properly extracts comment text when available and correctly attributes to the real user instead of the brid.gy relay.</li>
<li>I shipped a new release of <a href="https://github.com/msfjarvis/compose-lobsters/commit/a3d8d5b4d3dfd115b85dba7010393354543b14ee">Claw</a> this week, incorporating some UI feedback from <a href="https://abhinavsarkar.net/">Abhinav</a> and my first attempt at a (mostly) LLM generated feature: <a href="https://github.com/msfjarvis/compose-lobsters/commit/a3d8d5b4d3dfd115b85dba7010393354543b14ee">Temporary tag blocks</a>. I had to get my hands dirty at the end anyway because the LLM just completely forgot about data migration and integrity, but having it do 80% of the work while I was at the gym is pretty cool. I also have to admit that the UI flow that Claude came up with is miles better than anything that had been floating around in my brain. I don&rsquo;t know much it helped, but I used <a href="https://skills.sh/wshobson/agents/mobile-android-design">this &ldquo;skill&rdquo;</a> as part of the prompt.</li>
<li>Someone on the payroll team at Cloudflare probably discovered <a href="https://en.wikipedia.org/wiki/Idempotence">Idempotence</a> this week as everyone at our office got paid twice :P
<ul>
<li>Fret not, the extra money is already back with Cloudflare. Cheers to a brief taste of unearned wealth, the closest I will come to sharing an emotion with a billionaire.</li>
</ul>
</li>
<li>While trying to make that bank transfer I realized that my SIM had stopped working altogether, suspiciously after the Android 17 Beta 2 upgrade. Mild panicking ensued, and I had to add an additional side quest for a duplicate or a new SIM the next morning when I was meeting <a href="https://github.com/swapnilmadavi">Swapnil</a> and <a href="https://github.com/mayankofficial999">Mayank</a> for breakfast.
<ul>
<li>The boys helpfully accompanied me to the Jio store where I got the unfortunate news that the SIM owner has to be present in-person for a duplicate (which happened to be my mother), so I went to get a new Airtel SIM instead which was a relatively quick affair.</li>
<li>Of course right after that when I get home, the Jio SIM starts working again so all of the struggle was for naught. I fucking hate software sometimes.</li>
</ul>
</li>
<li>My weekly NixOS upgrades were basically impossible this week, there was a <a href="https://github.com/NixOS/nixpkgs/pull/494816">bad Linux regression</a>, and home-manager <a href="https://github.com/nix-community/home-manager/issues/8786">also has a severe bug</a> so I just reverted everything and will deal with it next week.</li>
</ul>
<hr />

<p>In case anyone is missing the food talk, this week was rather light on the experimentation front due to the aforementioned sad boy hours. I mostly ate salads and air fried a chicken breast or two a couple of times. I did try air fried potato wedges one night and they came out pretty nice, albeit slightly dry since I got the temperatures wrong. Lesson learned!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Weeknotes: Week #8 (2026)</title>
      <link>https://msfjarvis.dev/posts/weeknotes-week-8-2026/</link>
      <pubDate>Sun, 22 Feb 2026 20:56:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/weeknotes-week-8-2026/</guid>
      <description>The kitchen is my new comfort space</description>
      <content:encoded><![CDATA[<p>I’ll apologize upfront: I’m going to talk about food a lot and not share any pictures because I have the excuse of <a href="https://github.com/sveltia/sveltia-cms/issues/586">my CMS</a> not supporting the asset pipeline I want to establish. Soon™️</p>
<p>Now, onward to the real content. A lot of time was spent in the kitchen this week, with <a href="https://yashgarg.dev/">Yash</a> and me continuing our burrito escapades. On Monday, I decided to try air-frying some tortilla chips to see how it would go, and despite the obvious problems caused by overcrowding the rather tiny fryer basket, I’d consider it a success overall. Definitely a good option for dinner in a pinch.</p>
<p>A couple of days later, we went back in for even more burritos, building on our previous experience. The beans got a more generous round of seasoning, the rice was cooked and cooled ahead of time, and we were more mindful about how much we tried to shove into each burrito. This was remarkably successful, with both of us managing to create fully wrapped burritos by the end of the ordeal. I opted for a veggie-heavy burrito bowl with my remaining portions instead of wrestling with the tortilla again, which felt like the smarter choice. Throwing in some air-fried chicken sausages elevated the whole situation, but there was a definite lack of carbs, which I’ll have to keep in mind for the future.</p>
<p>On Friday, I had zero energy to cook, so I just chopped up the remaining lettuce along with some onions, tomatoes, and cucumbers, tossed it all in some sauces we had lying around, and pretended it was a gourmet salad. 10/10, would do again.</p>
<p>Yash is back home for a couple of weeks, which means I’m left to fend for myself in terms of sustenance. So I’m making a U-turn back to the Mediterranean phase of my life and already have big batches of falafel and hummus sitting in my fridge. This time I did remember to use the appropriate amount of greens and—shocked Pikachu face—it’s actually come out way nicer than my last mishap. Who knew following the recipe was the way?</p>
<p>With all the food talk out of the way, the boys and I have been getting back into <a href="https://www.playstation.com/games/helldivers-2">Helldivers 2</a>, and things have been <a href="https://androiddev.social/@msfjarvis/116087678608961557">going (not) great</a>. Super fun, though!</p>
<p>Relatedly, my laptop is back to its fortnightly cycle of throttling at the most inopportune moments, which has made gaming frustrating every so often. Since Windows is fairly out of my wheelhouse, I decided to commit the sin of letting an LLM replace the usual experience of stumbling through outdated forum articles. I had it run a gamut of debugging scripts to try and diagnose the problem. Everything that came out of this multi-day rigmarole is archived in <a href="https://git.msfjarvis.dev/msfjarvis/nvidia-struggles">this Git repository</a>. I’m not yet 100% confident that what Claude Sonnet 4.5 identified as the problem has truly resolved the issue, but it’s been fine for the past day or so.</p>
<p>Continuing the theme of tying things into the next entry, I have more LLM nonsense to share! I use <a href="https://jtx.techbee.at/">jtx Board</a> to manage my TODOs via CalDAV and wanted a simple way to show them in my <a href="https://github.com/glanceapp/glance">Glance</a> dashboard, which supports <a href="https://github.com/glanceapp/glance/blob/6c5b7a3f4cc409e31739b2914bb6636d08299126/docs/custom-api.md">custom APIs</a>—but unsurprisingly, only JSON. I had Claude try to write a simple proxy that could massage the CalDAV response into a suitable JSON format that I could plug into Glance. On its first pass, it took the credentials I had explicitly provided in a separate gitignored file for integration tests and put them into config.example.yml. When I asked it to remove them, it also included them in the LLM response text itself, causing them to be leaked twice instead. It also tried to claim that integration tests were passing when they obviously weren’t. Somewhere along the way, it lost the ability to issue write tool calls, which resulted in hilarious scenarios where it would attempt a write that never went through, run a test that still obviously failed, and then crash out about it. Fun for the whole family.</p>
<p><a href="https://underline.center/t/indiewebclub-20/720">IndieWebClub #20</a> was this weekend! I had missed the last two installments because I was out of Bengaluru, so I was super excited to catch up with people again. I met a bunch of new folks, had some great conversations—just a really good time. I would wholeheartedly recommend it to anyone who wants to have their own website, regardless of their technical expertise. You can get any level of help from the nicest, most patient people and become part of a small but growing community. It’s really special to me! The next one’s on the 7th of March—mark your calendars 🙂</p>
<p>And finally, this site now supports <a href="https://www.w3.org/TR/webmention/">WebMentions</a>! The server-side of this is built on top of the Cloudflare Developer Platform, leveraging Workers, the D1 SQLite database and Workers Queues. It was a nice way to finally dive back into Cloudflare&rsquo;s compute offerings since I had last used them in 2022 and I was quite pleased with how much better everything is now. A lot more things are now possible to run on Workers than before, and the local development experience is <strong>really</strong> good now. I&rsquo;ll probably build some more things on Workers in the future, and hopefully write about them.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Weeknotes: Week #7 (2026)</title>
      <link>https://msfjarvis.dev/posts/weeknotes-week-7-2026/</link>
      <pubDate>Sun, 15 Feb 2026 20:22:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/weeknotes-week-7-2026/</guid>
      <description>Back to work and getting really into burritos</description>
      <content:encoded><![CDATA[<p>This was my first week back in the office after an absence of 3 weeks which was a nice return to my usual routine. Catching up with 10 days of emails and messages wasn&rsquo;t as herculean of a task as I had expected :D</p>
<p>I was finally able to put in a replacement request for the defective Pixel Watch 4 I had purchased <a href="/posts/weeknotes-week-4-2026/">a couple of weeks ago</a> now that I was in Bengaluru and delivery was a more straightforward affair. Finding the chat option for this was an absolute nightmare trying to navigate Google&rsquo;s endless mazes but once I was in there it was a pretty easy process. Blue Dart&rsquo;s speed was quite the savior, and I got the replacement device back in just 3 days.</p>
<p>Now that I have a more capable fitness tracker I&rsquo;ve been trying to be more mindful about staying active, which has been going less than stellar because I had been away from the gym for nearly a month so building back my endurance and stamina is gonna take a few more days. Definitely going to try and push for 10K steps a day again.</p>
<p>On Wednesday I fell on my way back from the gym by getting my foot stuck in an uneven footpath, which left me with some light but painful bruises. Thankfully they seem to have buffed out over the week, and the only permanent damage is going to be to my phone case :')</p>
<p>On Friday, my roommate Yash and I decided to try out making burritos which were quite scuffed but ultimately rather delicious. The 8 inch burrito wraps that we got ended up being rather small, and the absence of any burrito wrapping skill between the two of us did not help the situation. Would like to try it again sometime if we can find bigger wraps, or maybe make our own.</p>
<p>Valentine&rsquo;s day was spent enjoying my own company and working on some homelab stuff I had been putting off. My Git server is constantly being spammed by Chinese and Vietnamese spammers, which I <a href="https://androiddev.social/@msfjarvis/116069080640569258">tried to mitigate unsuccessfully</a>. Will take another stab at it soon.</p>
<p>I also put out a <a href="https://github.com/msfjarvis/compose-lobsters/releases/tag/v1.60.0">new release of Claw</a> since it had been a month since the previous one, and it seems to have not exploded right away so that&rsquo;s a relief.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Weeknotes: Week #6 (2026)</title>
      <link>https://msfjarvis.dev/posts/weeknotes-week-6-2026/</link>
      <pubDate>Sun, 08 Feb 2026 19:39:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/weeknotes-week-6-2026/</guid>
      <description>Finally back in Bengaluru!</description>
      <content:encoded><![CDATA[<p>After 3 weeks of being away, I am finally back in Bengaluru this weekend. I was also off work for the past ~10 days, so I am looking forward to spending half of my Monday sorting through emails and unread Google Chat messages. I may never hit Inbox Zero ever again&hellip;</p>
<p>To start off the week, I finally got my first <a href="https://www.playbalatro.com/">Balatro</a>  win off an extremely lucky draw of jokers – one which gave &ldquo;mult&rdquo; equal to the sell value of all jokers and another that gained sell value each turn. I also hit an extremely lucky spectral card that replaced a whole hand with jesters. The seed for that run was <code>A7IBENU9</code>, in case anyone wants to show me up :P</p>
<p>I also introduced Balatro to my sister who got the hang of the Poker aspect of it, but doesn&rsquo;t quite get the Roguelike part yet.</p>
<p>Outside Balatro and the occasional ceremony for the wedding I was attending, I occupied my time in trying to finish reading the minor behemoth that was <a href="https://bookwyrm.social/book/1584147/s/book-of-ile-rien">Book of Ile-Rien</a>. When I finally got done with it, I realized I had basically every Martha Wells book on the Kobo <em>except</em> the sequels to the Ile-Rien series which I wanted to read next :&lsquo;D</p>
<p>The eBook edition was not available on the Kobo India store so I had to get it from Barnes &amp; Noble, then figure out how to transfer it to the Kobo which also became a quick note <a href="https://msfjarvis.dev/notes/til-easily-sending-ebooks-to-a-kobo/">on the blog</a>.</p>
<p>The flight back to Bengaluru was a Boeing which didn&rsquo;t quite inspire confidence, and the rather rough turbulence in the last 15 minutes of the journey had me genuinely fearing for my life but I made it down in one piece. I had to wait a while for the 3:30 PM bus back to Indiranagar but it was made pleasant by the AC lounge in the basement bus pickup area of the Kempegowda Airport.</p>
<p>Earlier in the week I had discovered (via <a href="https://indiewebify.me/">IndieWebify</a>) about the existence of the <a href="https://microformats.org/wiki/h-entry">h-entry</a> standard, and the site implements it now.</p>
<p>I was planning to be at <a href="https://underline.center/t/indiewebclub-19/708/1">IndieWebClub #19</a> today, having been away for the last 2 iterations of it but I woke up with the sniffles and didn&rsquo;t have the energy for it. Hopefully next time 🤞</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Weeknotes: Week #5 (2026)</title>
      <link>https://msfjarvis.dev/posts/weeknotes-week-5-2026/</link>
      <pubDate>Sun, 01 Feb 2026 20:39:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/weeknotes-week-5-2026/</guid>
      <description>I swear all this travel is against my will</description>
      <content:encoded><![CDATA[<p>This week was mostly spent back in my ancestral village with effectively zero internet, so this weeknote is being typed up on my phone at 8:40 PM because I completely forgot about it.</p>
<p>The most interesting part of my week was me getting involved in some Discourse™ over a Cloudflare blog post that made dubious claims about a vibe coded Matrix implementation that was written to run on Workers. Needless to say I will not be exercising my &ldquo;Disclaimer: I&rsquo;m a CF employee but do not speak for the company&rdquo; rights in the future.</p>
<p>On a positive note despite the biting cold and fog I&rsquo;ve been able to get a solid 3-4 hours in the sun every day which were often translated into <em>heavenly</em> naps. To put it in perspective how rare it is for me to sleep out of turn, I&rsquo;ve owned my Amazfit tracker for 2 years and this week is when I first discovered it tracks naps. During all the sun bathing I also managed to get through the first half of <a href="https://en.wikipedia.org/wiki/Ile-Rien#The_Element_of_Fire">The Book of Ile-Rien</a>.</p>
<p>And that&rsquo;s kinda it! Having little to no access to the Internet has been kind of driving me crazy but I preloaded the Kobo with enough material to have no excuse to not just be reading, so that&rsquo;s what I&rsquo;ve been using my time for. The entirety of next week is going to be wrapped up in the festivities for the wedding I&rsquo;m here for so I expect to have another light weeknote which will thankfully be written from Bengaluru :)</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Weeknotes: Week #4 (2026)</title>
      <link>https://msfjarvis.dev/posts/weeknotes-week-4-2026/</link>
      <pubDate>Sun, 25 Jan 2026 15:46:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/weeknotes-week-4-2026/</guid>
      <description>A slow week back at home</description>
      <content:encoded><![CDATA[<p>I&rsquo;m still back at home so there&rsquo;s not been a whole lot happening with my life other than being unable to deal with the cold.</p>
<ul>
<li>
<p>I started reading <a href="https://bookwyrm.social/book/1584147/s/book-of-ile-rien">The Book of Ile-Rien</a>, without realizing that the copy I bought is actually a combined version of <a href="https://en.wikipedia.org/wiki/Ile-Rien#The_Element_of_Fire">The Element of Fire</a> and <a href="https://en.wikipedia.org/wiki/Ile-Rien#Death_of_the_Necromancer">Death of the Necromancer</a> so I have a 900+ page behemoth on my hand that is a departure from the  science fiction fare I usually consume. In six chapters I&rsquo;m up to 17 unique words describing medieval things that I had to look up so overall this has been quite the experience.</p>
</li>
<li>
<p>The Pixel Watch I ordered last week came in on Tuesday and by Thursday its display had already developed horrid discoloration that forced me to put it in for an <a href="https://en.wikipedia.org/wiki/Return_merchandise_authorization">RMA</a> and get a replacement. It&rsquo;s particularly bad timing since I will be traveling to my hometown soon for a marriage and then head back to Bengaluru, so I tried to have it be picked up from here and get it delivered in Bengaluru. Unfortunately the support executive I talked to got the wires crossed on that and scheduled the pickup from Bengaluru 🥲. Still trying to get that sorted.</p>
</li>
<li>
<p>We had a fair amount of thunder and rains on Friday which immediately knocked out both me and the power grid. The power came back 7-8 hours later but I continue to have the worst cold.</p>
</li>
</ul>
<p>Being away from work for a couple of weeks now hasn&rsquo;t been as isolating as I would&rsquo;ve expected which says good things about the team, but it has still been quite the drudgery being stuck inside what with the cold and the rain. I already miss the fabled Bengaluru weather&hellip;</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Weeknotes: Week #3 (2026)</title>
      <link>https://msfjarvis.dev/posts/weeknotes-week-3-2026/</link>
      <pubDate>Sun, 18 Jan 2026 12:20:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/weeknotes-week-3-2026/</guid>
      <description>A relatively quiet week mostly spent traveling and reading</description>
      <content:encoded><![CDATA[<p>As I mentioned <a href="/posts/weeknotes-week-2-2026/">last week</a>, I&rsquo;ve been traveling most of this week to attend the last rites of some immediate family that passed away at the start of this year. This has meant limited access to my computer and taking the week off work which might&rsquo;ve been a blessing in some part.</p>
<ul>
<li>When I was leaving Bangalore, I tried taking a <a href="https://kia.bengawalk.com/">KIA BengaWalk</a> electric bus to the airport but it never showed up so I ended up in an expensive taxi instead. My experience with the bus network was pretty good when I used it last time to return from the airport so this was quite disappointing.</li>
<li><a href="http://obsidian.md/">Obsidian</a> updated this week to use rounded elements everywhere which I have mixed feelings about. It hasn&rsquo;t felt too disruptive this week but I also didn&rsquo;t use it all that much due to being mostly offline, so we&rsquo;ll see in the next couple of weeks what side of the fence I end up on.</li>
<li>Finished reading <a href="https://bookwyrm.social/book/225118/s/shift">Shift</a> on the flight to Varanasi.</li>
<li>Immediately picked up <a href="https://bookwyrm.social/book/65228/s/dust">Dust</a> once I reached the village.</li>
<li>The generally poor mobile network at the village exposed a failure condition for the home screen widget in my app <a href="https://github.com/msfjarvis/compose-lobsters/">Claw</a>, so I took a crack at having <a href="https://github.com/copilot">Copilot</a> implement a caching feature to make the updates tolerant of transiently bad network. It took some prodding to make it all work, but in the end I was mostly satisfied with its ability to copy existing code <a href="https://github.com/msfjarvis/compose-lobsters/pull/1041">[PR]</a>.</li>
<li>The day before we were supposed to head back home, I got roped into assembling wedding invites with my Dad which ended up taking most of our time but it was nice to catch up with Dad for an extended period that I usually don&rsquo;t get the opportunity for.</li>
<li>We ended up splitting our drive back home with a pit stop in Allahabad at a family friend, and I ashamedly used that time to catch up with work. I had completely forgotten about the Q1 kickoff meetings so there&rsquo;s a couple of hours of recordings to watch when I&rsquo;m back next week.</li>
<li>We left bright and early for the drive back which was rather rough with the heavy fog. I used the time in the car to finish off reading <a href="https://bookwyrm.social/book/65228/s/dust">Dust</a>, which might&rsquo;ve been the fastest I&rsquo;ve gotten through 300+ pages. The Silo series is written in quite a cliffhanger-heavy style where every chapter keeps you hooked, so I found it hard to put down. No complaints though, I loved all 3 books.</li>
<li>My Amazfit fitness tracker fell apart <a href="/posts/weeknotes-week-1-2026/">again</a> so I finally ordered the Pixel Watch 4 I had been eyeing, with a <a href="https://www.amazon.in/dp/B0FWT8ND63">sturdy case</a> to protect it from my neglect.</li>
</ul>
]]></content:encoded>
    </item>
    
    <item>
      <title>Weeknotes: Week #2 (2026)</title>
      <link>https://msfjarvis.dev/posts/weeknotes-week-2-2026/</link>
      <pubDate>Sun, 11 Jan 2026 19:38:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/weeknotes-week-2-2026/</guid>
      <description>Rounding up a rather depressing week</description>
      <content:encoded><![CDATA[<p>This week has been quite difficult on the mental, with the death of my maternal grandmother and my childhood best friend&rsquo;s father in quick succession. I&rsquo;ll be going home for their last rites next week, and probably miss the weeknote posting schedule. Despite the horrible start, other things did happen during the rest of the days as listed out below.</p>
<ul>
<li>I had to shave my beard because of some skin irritation that I am medicating for, which apparently was enough to confuse my landlord into wondering who I was.</li>
<li>As I am in a quite Mediterranean time of my life, another batch of hummus was created, this time with a lot of more spices and it came out quite delicious indeed.</li>
<li>My phone has had the &ldquo;pink line of dead pixels&rdquo; syndrome for a while but it finally became enough of a problem for me to get it repaired, and in the process Android&rsquo;s <a href="https://support.google.com/pixelphone/answer/14266732?hl=en">Repair Mode</a> bugged out and forced me to wipe my device to get it out of repair mode. Quite the annoyance to put onto a person already in the dumps, made worse by a Firefox bug that <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=2009251">caused passkeys to not work</a>.</li>
<li>The aforementioned data wipe also exposed a bug in <a href="https://github.com/msfjarvis/compose-lobsters/">Claw</a> which prevented auto backup from working, so at  least <a href="https://github.com/msfjarvis/compose-lobsters/commit/e07bd441a8801bbb5b633636076dc36a154fe30d">something good</a> came out of me losing 3 months of saved posts since I had last backed things up manually.</li>
<li><a href="https://www.linkedin.com/in/willigeiger">Willi Geiger</a>, the head of the Cloudflare Media team was visiting the Bengaluru office this week. He took a session explaining the Media business as well as 1:1 meetings with every one of us who works in the Media team. Was nice having him around, shadowing how we do things in the Bengaluru office.</li>
<li>Watched <a href="https://en.wikipedia.org/wiki/Avatar%3A_Fire_and_Ash">Avatar Fire and Ash</a> with a couple of friends, as usual it&rsquo;s a great spectacle with a flimsy story. Hollywood, keep giving this guy money to justify the existence of IMAX.</li>
<li>Last week I had ordered some accessories for the <a href="https://www.ikea.com/in/en/cat/skadis-series-37813/">IKEA Pegboard</a> which arrived on Saturday, and I was finally able to fill out the thing and put up some of the stuff that was cluttering my desk.</li>
<li><a href="https://yashgarg.dev">Yash</a> and I have been experimenting on the cooking front to start making more things than our usual 4 dishes and made some <a href="https://www.vegrecipesofindia.com/baingan-bharta-recipe-punjabi-baingan-bharta-recipe/">baingan bharta</a> on Saturday. Definitely burnt some of my fingertips off while roasting and peeling the eggplants but completely worth it.</li>
</ul>
]]></content:encoded>
    </item>
    
    <item>
      <title>Migrating my website&#39;s analytics from Plausible to Umami</title>
      <link>https://msfjarvis.dev/posts/migrating-my-website-s-analytics-from-plausible-to-umami/</link>
      <pubDate>Sat, 10 Jan 2026 00:40:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/migrating-my-website-s-analytics-from-plausible-to-umami/</guid>
      <description>Frustrated by ClickHouse&amp;rsquo;s high disk usage, I ditched Plausible for Umami</description>
      <content:encoded><![CDATA[<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>
<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&rsquo;t verify the hypothesis.</p>
<p>The migration itself was quite simple so I&rsquo;ll reproduce the steps below for any interested people looking for migration steps.</p>
<ul>
<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.
<ul>
<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>
</ul>
</li>
<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>
<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>
<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>
<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>
<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>
</ul>
<p>And that&rsquo;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&rsquo;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="/posts/migrating-from-simple-analytics-to-self-hosted-plausible/">Plausible only lasted 8 months</a> before I moved out of it. I should&rsquo;ve been more wary of ClickHouse than I was of MySQL when I chose Plausible over Matomo :)</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Weeknotes: Week #1 (2026)</title>
      <link>https://msfjarvis.dev/posts/weeknotes-week-1-2026/</link>
      <pubDate>Sat, 03 Jan 2026 16:20:58 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/weeknotes-week-1-2026/</guid>
      <description>Starting off the year being VERY lazy</description>
      <content:encoded><![CDATA[<ul>
<li>
<p>I put out a new post this week <a href="/posts/coming-around-on-the-utility-of-llms/">on my changing my stance on LLMs as coding assistants</a>. I talked myself out of it a few times because the discourse around generative AI is so polarized but I ended up deciding that I shouldn&rsquo;t have to censor myself for fear of some angry mentions on Mastodon.</p>
</li>
<li>
<p>The Steam Winter Sale is ending early next week, so I finally pulled the trigger and bought the stuff in my cart, likely to sit in my backlog forever. I ended up cutting out ARC Raiders because the extraction shooter genre isn&rsquo;t very appealing to me. The games I did buy are listed below:</p>
<ol>
<li>Balatro</li>
<li>PEAK</li>
<li>Slay The Spire</li>
<li>The Witcher trilogy</li>
<li>No Mans Sky</li>
</ol>
</li>
<li>
<p>Of the games I bought, Balatro ended up being the obvious first pick and has replaced Marvel Snap as my casual 15 minute game. I&rsquo;m pretty sad that the mobile versions don&rsquo;t sync with my Steam copy of the game so I don&rsquo;t play it on mobile anymore.</p>
</li>
<li>
<p>My sister called me out of the blue asking why her laptop is not as easily repairable anymore, and we had quite a nice conversation about <a href="https://en.wikipedia.org/wiki/Enshittification">enshittification</a> and <a href="https://en.wikipedia.org/wiki/Planned_obsolescence">planned obsolescence</a>. Pretty proud of her for being so cognizant about the state of consumerism in everyday life.</p>
<ul>
<li>We ended up having another conversation later in the week around copyright law, because she wanted to find a book on <a href="https://en.wikipedia.org/wiki/Anna%27s_Archive">Anna&rsquo;s Archive</a> and was distraught to discover that the only domain she was aware of had been blocked. Having to explain why domains people paid for can just be taken away was, to say the least, <em>interesting</em>.</li>
</ul>
</li>
<li>
<p>My cheap and reliable fitness tracker, the <a href="https://www.amazon.in/dp/B094NDV47Z">Amazfit GTS 2e</a>, <em>almost</em> succumbed to its 2 years of abuse. Thankfully I was able to seal it back shut with some superglue, but the vibration motor no longer works which means I don&rsquo;t get reminded to get off my ass every hour and the alarm functionality is useless. Maybe this is the universe&rsquo;s way of reinforcing my financially irresponsible desire of buying a Pixel Watch and adding <a href="https://wearos.google.com/">WearOS</a> to my skill set.</p>
</li>
</ul>
<figure>
    <img loading="lazy" src="my-broken-watch.webp"
         alt="A picture of the aforementioned watch on my arm, with its display precariously detached from the rest of the body and only attached on the left side. The watch is kind of scuffed and filthy." height="480px"/> 
</figure>

<ul>
<li>
<p>I <a href="https://bookwyrm.social/user/msfjarvis/generatednote/9326045">started reading</a> the next part of the <a href="https://en.wikipedia.org/wiki/Silo_(series)">Silo series</a>: <a href="https://en.wikipedia.org/wiki/Silo_(series)#Shift">Shift</a>. With the constant power cuts happening here in Indiranagar, this book on my Kobo has been the only thing I can do reliably without the threat of an arbitrary interruption.</p>
</li>
<li>
<p><a href="https://yashgarg.dev/">Roomie</a> and I met up with <a href="https://github.com/feniljain">Fenil</a> this week for some tea and snacks, was lovely catching up and yapping about stuff. Hopefully we can make it a regular thing!</p>
</li>
</ul>
]]></content:encoded>
    </item>
    
    <item>
      <title>Coming around on the utility of LLMs</title>
      <link>https://msfjarvis.dev/posts/coming-around-on-the-utility-of-llms/</link>
      <pubDate>Fri, 02 Jan 2026 22:23:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/coming-around-on-the-utility-of-llms/</guid>
      <description>Having unsuccessfully tried to use Windsurf and GitHub Copilot over the years, OpenCode finally made LLMs useful to me</description>
      <content:encoded><![CDATA[<blockquote>
<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>
</blockquote>
<p>For the longest time I&rsquo;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>
<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>
<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 &ldquo;agents&rdquo; 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&rsquo;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>
<p>I&rsquo;ve only started using OpenCode outside work in the past week or two, but the value add is undeniable. Here&rsquo;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>
<ul>
<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>
<li>It helped me add <a href="https://github.com/msfjarvis/compose-lobsters/commit/7f5dbf79e370e586068c3d2a5031a8d199d398f2">Sentry instrumentation for the app&rsquo;s database</a> which was previously missing and I kept struggling to understand the right APIs for it.</li>
<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&rsquo;t break during my rowdy refactoring.</li>
<li>It fixed <a href="https://github.com/msfjarvis/compose-lobsters/commit/b3373473a79679fb1d260cbd1219fd0208f82bf3">another crash</a> that hit users of the app&rsquo;s home screen widget</li>
<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>
<li>It fixed <a href="https://github.com/msfjarvis/compose-lobsters/commit/ab872b6eaa3c8aa8e5e751f15373f4f5ae1023a5">my poorly written deep linking implementation</a></li>
<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>
</ul>
<p>None of these are things that would be impossible for me to figure out, but they sure as hell wouldn&rsquo;t have been done in the span of two weeks. I don&rsquo;t really subscribe to the idea of fully phoning it in, the so called &ldquo;vibecoding&rdquo; aspect of using an LLM as a coding assistant. In every case where the LLM has solved a bug, I&rsquo;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>
<p>With the <a href="https://docs.sentry.io/product/sentry-mcp/">Sentry MCP</a> server, I can spin up an OpenCode session, say &ldquo;Investigate the root cause for Sentry issue &lt;id&gt; and include references in the summary&rdquo; 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>
<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>&rsquo;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>
<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&rsquo;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>
<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&rsquo;s xAI, <a href="https://www.seangoedecke.com/grok-deepfakes/">sexual harassment of minors</a>.</p>
<p>This is close to devolving into rambling, so I&rsquo;ll end it here. I don&rsquo;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&rsquo;ll use them for my personal projects. When they stop, I&rsquo;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>
]]></content:encoded>
    </item>
    
    <item>
      <title>Weeknotes: Week #52 (2025)</title>
      <link>https://msfjarvis.dev/posts/weeknotes-week-52-2025/</link>
      <pubDate>Sun, 28 Dec 2025 20:05:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/weeknotes-week-52-2025/</guid>
      <description>The final edition of the 2025 weeknotes</description>
      <content:encoded><![CDATA[<ul>
<li>With some help from my roommate, I&rsquo;ve started doing some proper weight training at the gym to speed up my weight loss journey and help build up some muscle definition. Way too early for any results, but the burn of a good workout feels nice until the soreness sets in :P</li>
<li>The Periodic Notes plug in I use for Obsidian has a bug with how it handles week alignments (<a href="https://github.com/liamcain/obsidian-periodic-notes/issues/238">here</a>) which prevented the &ldquo;Open weekly note&rdquo; command from working. Thankfully I realized that Obsidian plugins are just regular files in my vault so I was able to go in and fix the bug and have it fully work again!</li>
<li>My Telegram account was suspended last week under dubious circumstances, and I was finally able to get it back on Thursday by appealing the suspension again. Apparently this time round my appeal got accepted in 1 minute flat, but I didn&rsquo;t notice until way later in the day because I had no expectations from them.</li>
<li>I&rsquo;ve been having a bit of a monster week working on <a href="https://github.com/msfjarvis/compose-lobsters/">Claw</a>, my Android client for <a href="https://lobste.rs">Lobsters</a>. I fixed every single crash logged on Sentry, did a long-pending migration to drop a deprecated dependency injection library, and added a chunky suite of end-to-end tests using <a href="https://maestro.dev">Maestro</a> to paper over the lack of unit testing. Soon™</li>
<li>Making hummus at home has been a longstanding goal of mine which I finally acted on this week, and it came out quite nice! Originally I planned to make my own <a href="https://en.wikipedia.org/wiki/Tahini">Tahini</a> as well but I realized quite quickly that I don&rsquo;t have the patience to roast sesame seeds for an hour :)</li>
</ul>
<p>I&rsquo;m writing this at 9 PM on a Sunday which for a weeknote feels like turning in an assignment at 11:59 PM on the due date, going forward I&rsquo;ll try to block a chunk of time earlier in the day to work on this. The <a href="https://msfjarvis.dev/notes/collating-entries-in-my-obsidian-journal-for-week-notes/">Obsidian setup I created</a> to help with these weeknotes is still coming in quite handy, makes me quite happy that I chose to <a href="https://msfjarvis.dev/posts/migrating-from-logseq-to-obsidian/">migrate out of Logseq</a> earlier this year.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Weeknotes: Week #51 (2025)</title>
      <link>https://msfjarvis.dev/posts/weeknotes-week-51-2025/</link>
      <pubDate>Sat, 20 Dec 2025 16:25:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/weeknotes-week-51-2025/</guid>
      <description>My very first weeknotes, ironically for the second last week of the year :D</description>
      <content:encoded><![CDATA[<p>I got inspired by <a href="https://abhinavsarkar.net/notes/tags/weeknotes/">Abhinav</a> and <a href="https://ankursethi.com/">Ankur</a> last week to start writing weeknotes, so I spent that weekend setting up my existing journaling system to expose weeknote-relevant entries in a nice single page view (you can read about that <a href="https://msfjarvis.dev/notes/collating-entries-in-my-obsidian-journal-for-week-notes/">here</a>). This is the first weeknote as a direct result of that, being written at <a href="https://underline.center/t/indiewebclub-16-with-ankur-and-tanvi/622">IndieWebClub #16</a>!</p>
<ul>
<li>I finished my personal reading goal of 24 books this year, being tracked <a href="https://bookwyrm.social/user/msfjarvis/goal/2025">here</a>. Next year I wanna blend in a bit more non-fiction into this, and hopefully raise the count by a few more.</li>
<li>Continuing on the book front, I finished the last two short stories from the Murderbot series: <a href="https://torpublishinggroup.com/home-habitat-range-niche-territory/?isbn=9781250838865&amp;format=ebook">Home: Habitat, Range, Niche, Territory</a> and <a href="https://torpublishinggroup.com/rapport-friendship-solidarity-communion-empathy/?isbn=9781250425362&amp;format=ebook">Rapport: Friendship, Solidarity, Communion, Empathy</a>, and started reading <a href="https://en.wikipedia.org/wiki/Silo_(series)#Wool">Wool</a>.</li>
<li>There was an alumni dinner for <a href="https://www.obvious.in/">Obvious</a> on Wednesday where we ended up with 20+ people at <a href="https://www.idylllindia.com/">Idylll</a>. Met a lot of people from before my time at Obvious, and caught up with the ones I did work with. Super fun experience, was very glad that I could make it.</li>
<li>My Telegram account got banned for no particular reason, likely by mass reporting by someone I pissed off and forgot about? I&rsquo;ve reconvened on Discord for now but it&rsquo;s clearly a problem that my primary communication platform can just evaporate overnight. I can set something up for myself but moving over friends is gonna be the real pain.</li>
<li>I watched <a href="https://en.wikipedia.org/wiki/Dhurandhar">Dhurandhar</a> on Thursday with a couple of friends and it was great! Bit more gory than I expected from Bollywood and the CGI was a little iffy but the story and acting more than make up for it.</li>
<li>I tried getting back into <a href="https://www.hollowknight.com/">Hollow Knight</a> but the combat is not really working for me, so I&rsquo;ll let the roguelike fever die down for a bit and pick up <a href="https://en.wikipedia.org/wiki/Control_(video_game)">Control</a> or <a href="https://en.wikipedia.org/wiki/Horizon_Forbidden_West">Horizon Forbidden West</a> which I&rsquo;ve been sitting on for a while.</li>
<li>At work I&rsquo;ve mostly been busy with the <a href="https://kotlinlang.org/docs/whatsnew23.html">Kotlin 2.3.0</a> upgrade which has a lot performance improvements and additions relevant to our needs. I was able to get everything to work, but unfortunately can&rsquo;t ship it without a compatible <a href="https://github.com/touchlab/SKIE/issues/173">SKIE</a> release. Hopefully soon!</li>
<li>I finally added search to this blog! It&rsquo;s something I had wanted for a long while and being at IndieWebClub finally motivated me to execute on it. I&rsquo;m pretty happy with the result, but I intend to customize the UI and search result ranking a bit since I saw some instances of tag pages being prioritized over the actual blog post using the tag.</li>
</ul>
]]></content:encoded>
    </item>
    
    <item>
      <title>My experience at droidcon India 2025</title>
      <link>https://msfjarvis.dev/posts/my-experience-at-droidcon-india-2025/</link>
      <pubDate>Sun, 14 Dec 2025 12:17:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/my-experience-at-droidcon-india-2025/</guid>
      <description>I finally stepped out of my house! And met people! So cool!</description>
      <content:encoded><![CDATA[<p><img alt="My droidcon 2025 badge with my name, company and role." loading="lazy" src="/posts/my-experience-at-droidcon-india-2025/droidcon-2025-badge.webp" title="My droidcon 2025 badge"></p>
<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&rsquo;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>
<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>
<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&rsquo;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>
<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>
<p>Another short break and then one of my highlights for the day: <a href="https://www.linkedin.com/in/rahulrav/">Rahul Ravikumar</a>&rsquo;s &ldquo;A Busy Android App Developer&rsquo;s Guide to Perfetto&rdquo;. Inspired by <a href="https://lalitm.com/">Lalit Maganti</a>&rsquo;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&rsquo;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>
<p>The next talk I attended was <a href="https://www.linkedin.com/in/himanshoe/">Himanshu Singh</a> speaking about &ldquo;Building Android Open Source Libraries: Managing Public APIs with Intention&rdquo;. 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>
<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&rsquo;s now building <a href="https://tramline.app">Tramline</a> to make mobile releases as streamlined as deploying to the web. I&rsquo;m a happy solo user of the product, but am otherwise not paid to shill it here :P</p>
<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>
<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>&rsquo;s talk: &ldquo;The Art of Re-architecture: Lessons from the Trenches at Foodpanda &amp; Delivery Hero&rdquo;. 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 &ldquo;Refactor for today, Re-architect for tomorrow&rdquo; was particularly inspiring as the resident refactorman of my team 👌</p>
<p>Next up was <a href="https://linkedin.com/in/hadiyarajesh">Rajesh Hadiya</a> with &ldquo;Build Kotlin Compiler Plugins for Production level Android Apps&rdquo;. Despite the extremely dense topic he did a great job at making it fun and approachable. I&rsquo;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>
<p>We skipped the 3:45 PM to 04:05 PM slot as well due to nothing particularly appealing to the group.</p>
<p><a href="https://www.linkedin.com/in/omkartenkale/">Omkar Tenkale</a>&rsquo;s talk titled &ldquo;Building the Coroutine Framework from Scratch&rdquo; 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&rsquo;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&rsquo;s was easily my second favorite talk here after Rahul&rsquo;s Perfetto deep dive.</p>
<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 &ldquo;Cross-Platform Bluetooth Architecture for Android, iOS, and Embedded Devices&rdquo; for the Meta RayBan glasses. It was expected yet disappointing that we didn&rsquo;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>
<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>
<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&rsquo;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>
]]></content:encoded>
    </item>
    
    <item>
      <title>Creating private services on NixOS using Tailscale and Caddy</title>
      <link>https://msfjarvis.dev/posts/creating-private-services-on-nixos-using-tailscale-and-caddy/</link>
      <pubDate>Sat, 13 Sep 2025 21:18:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/creating-private-services-on-nixos-using-tailscale-and-caddy/</guid>
      <description>A simple guide to setting up private services on NixOS using Tailscale and Caddy with authentication.</description>
      <content:encoded><![CDATA[<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&rsquo;s a quick run down of how to set this up on NixOS.</p>
<h2 id="installing-caddy-tailscale-for-nixos">Installing caddy-tailscale for NixOS</h2>
<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&rsquo;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>
<blockquote>
<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&rsquo;ve done is checked it for obviously silly shit.</p>
</blockquote>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl">   services.caddy = {
</span></span><span class="line"><span class="cl">     enable = true;
</span></span><span class="line"><span class="cl"><span class="gi">+    package =
</span></span></span><span class="line"><span class="cl"><span class="gi">+      pkgs.caddy.withPlugins {
</span></span></span><span class="line"><span class="cl"><span class="gi">+        plugins = [
</span></span></span><span class="line"><span class="cl"><span class="gi">+          &#34;github.com/jasonlovesdoggo/caddy-defender@v0.8.5&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+          &#34;github.com/tailscale/caddy-tailscale@v0.0.0-20250207163903-69a970c84556&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+        ];
</span></span></span><span class="line"><span class="cl"><span class="gi">+        hash = &#34;sha256-z+zj3rfXbyxldRjO1yoLD77ACRWEAofzMDiZe/bHAqw=&#34;;
</span></span></span><span class="line"><span class="cl"><span class="gi">+     }
</span></span></span><span class="line"><span class="cl">     globalConfig = &#39;&#39;
</span></span><span class="line"><span class="cl">       servers {
</span></span></code></pre></div><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&rsquo;s an example of how to do so using <a href="https://github.com/Mic92/sops-nix"><code>sops-nix</code></a>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="n">sops</span><span class="o">.</span><span class="n">secrets</span><span class="o">.</span><span class="n">services-tsauthkey-env</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">sopsFile</span> <span class="o">=</span> <span class="sr">./secrets/tailscale.env</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">owner</span> <span class="o">=</span> <span class="n">config</span><span class="o">.</span><span class="n">services</span><span class="o">.</span><span class="n">caddy</span><span class="o">.</span><span class="n">user</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">format</span> <span class="o">=</span> <span class="s2">&#34;dotenv&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="n">services</span><span class="o">.</span><span class="n">caddy</span><span class="o">.</span><span class="n">environmentFile</span> <span class="o">=</span> <span class="n">config</span><span class="o">.</span><span class="n">sops</span><span class="o">.</span><span class="n">secrets</span><span class="o">.</span><span class="n">services-tsauthkey-env</span><span class="o">.</span><span class="n">path</span><span class="p">;</span>
</span></span></code></pre></div><p>Create the <code>secrets/tailscale.env</code> file with sops, the contents should look something like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">TS_AUTHKEY=tskey-auth-something-something
</span></span></code></pre></div><h2 id="setting-up-your-first-private-service">Setting up your first private service</h2>
<p>Now we&rsquo;re ready to rumble. To showcase the full capabilities of <code>caddy-tailscale</code>, we&rsquo;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&rsquo;s a simple example (N.B. <code>$TAILNET_NAME</code> should be replaced with your Tailscale network name.):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="n">services</span><span class="o">.</span><span class="n">caddy</span><span class="o">.</span><span class="n">virtualHosts</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;https://</span><span class="si">${</span><span class="n">config</span><span class="o">.</span><span class="n">services</span><span class="o">.</span><span class="n">grafana</span><span class="o">.</span><span class="n">settings</span><span class="o">.</span><span class="n">server</span><span class="o">.</span><span class="n">domain</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">extraConfig</span> <span class="o">=</span> <span class="s1">&#39;&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">      # This will create a new node at grafana.$TAILNET_NAME.ts.net
</span></span></span><span class="line"><span class="cl"><span class="s1">      bind tailscale/grafana
</span></span></span><span class="line"><span class="cl"><span class="s1">      # Enables the Tailscale authentication provider
</span></span></span><span class="line"><span class="cl"><span class="s1">      tailscale_auth
</span></span></span><span class="line"><span class="cl"><span class="s1">      # Forwards your Tailscale user ID to Grafana. Usually your email address.
</span></span></span><span class="line"><span class="cl"><span class="s1">      reverse_proxy </span><span class="si">${</span><span class="n">config</span><span class="o">.</span><span class="n">services</span><span class="o">.</span><span class="n">grafana</span><span class="o">.</span><span class="n">settings</span><span class="o">.</span><span class="n">server</span><span class="o">.</span><span class="n">http_addr</span><span class="si">}</span><span class="s1">:</span><span class="si">${</span><span class="nb">toString</span> <span class="n">config</span><span class="o">.</span><span class="n">services</span><span class="o">.</span><span class="n">grafana</span><span class="o">.</span><span class="n">settings</span><span class="o">.</span><span class="n">server</span><span class="o">.</span><span class="n">http_port</span><span class="si">}</span><span class="s1"> {
</span></span></span><span class="line"><span class="cl"><span class="s1">        header_up X-Webauth-User {http.auth.user.tailscale_user}
</span></span></span><span class="line"><span class="cl"><span class="s1">      }
</span></span></span><span class="line"><span class="cl"><span class="s1">    &#39;&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">services</span><span class="o">.</span><span class="n">grafana</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">enable</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">settings</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;auth.proxy&#34;</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># Enable proxy-based authentication</span>
</span></span><span class="line"><span class="cl">      <span class="n">enabled</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># Automatically create new accounts for people who access the service</span>
</span></span><span class="line"><span class="cl">      <span class="n">auto_sign_up</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># Do not store login cookies, instead always relying on proxy authentication</span>
</span></span><span class="line"><span class="cl">      <span class="n">enable_login_token</span> <span class="o">=</span> <span class="no">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="n">server</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">domain</span> <span class="o">=</span> <span class="s2">&#34;grafana.$TAILNET_NAME.ts.net&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">http_addr</span> <span class="o">=</span> <span class="s2">&#34;127.0.0.1&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">http_port</span> <span class="o">=</span> <span class="mi">2342</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span></code></pre></div><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>
<figure>
    <img loading="lazy" src="grafana-profile.webp"
         alt="The profile page on Grafana, showing the name, email and username fields with an additional &#39;synced via auth proxy&#39; label next to them. The email and username have been blurred out to redact them for privacy reasons"/> <figcaption>
            Grafana profile showing that user identification is being done via the Tailscale proxy
        </figcaption>
</figure>

<h2 id="caveats">Caveats</h2>
<p>While this approach works great for most needs, there are a few caveats to be aware of:</p>
<ul>
<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>
<li>Since each &ldquo;virtual&rdquo; device being created via this is technically backed by your physical machine, the &ldquo;Services&rdquo; 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>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>Despite its experimental status, <code>caddy-tailscale</code> is pretty powerful and exposes enough functionality to cover most basic use cases. I&rsquo;ve been pretty happy running it in my homelab for the past couple months and I&rsquo;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://androiddev.social/@msfjarvis">Mastodon</a>.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Migrating from Gitea to Forgejo the long way</title>
      <link>https://msfjarvis.dev/posts/migrating-from-gitea-to-forgejo-the-long-way/</link>
      <pubDate>Mon, 01 Sep 2025 19:32:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/migrating-from-gitea-to-forgejo-the-long-way/</guid>
      <description>With the renewed interest in Forgejo I decided to finally pull the plug on moving out of Gitea, and this is how it went.</description>
      <content:encoded><![CDATA[<p>I&rsquo;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&rsquo;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>
<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&rsquo;s &ldquo;CoreAI&rdquo; division was the straw that broke the camel&rsquo;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>
<blockquote>
<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>
</blockquote>
<h1 id="attempt-1-the-naive-way">Attempt 1: The naive way</h1>
<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&rsquo;s migration section</a> suggested that this was possible, but it&rsquo;s actually missing crucial information about version requirements that was <a href="https://github.com/NixOS/nixpkgs/commit/91947bb68e8184eba4c14476a6a14873f15e9ed4">added later</a>.</p>
<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>
<h1 id="attempt-2-the-gitea-dump-command">Attempt 2: The <code>gitea dump</code> command</h1>
<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>
<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>
<p>Unfortunately this didn&rsquo;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>
<h1 id="attempt-3-vibe-coding-deployed-somewhat-effectively">Attempt 3: Vibe coding deployed somewhat effectively</h1>
<blockquote>
<p>I&rsquo;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&rsquo;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>
</blockquote>
<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 &ldquo;popular open source maintainer&rdquo;</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>
<blockquote>
<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>
</blockquote>
<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>
<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>
<h3 id="inability-to-log-into-the-gitea-instance">Inability to log into the Gitea instance.</h3>
<p>I could no longer sign into Gitea since I had 2FA enabled using Passkeys which require the domains to match up.</p>
<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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl">sudo -i su - gitea
</span></span><span class="line"><span class="cl"># To get the actual path to the Gitea binary, which weirdly isn&#39;t installed for the gitea user?
</span></span><span class="line"><span class="cl">systemctl cat gitea.service | grep ExecStart=
</span></span><span class="line"><span class="cl">/nix/store/foo-bar-baz-gitea-1.23.1/bin/gitea admin -w $(pwd) user generate-access-token --token-name forgejo-migration --scopes &#34;read:repository,read:user&#34; --raw
</span></span></code></pre></div><p>On the forgejo side, a similar dance ensued but this time to actually create my account since this was a fresh start.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl">sudo -i su - forgejo
</span></span><span class="line"><span class="cl">systemctl cat forgejo.service | grep ExecStart=
</span></span><span class="line"><span class="cl"># yes the actual CLI seems to still be available as gitea
</span></span><span class="line"><span class="cl">/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 &#34;read:user,write:repository&#34;
</span></span></code></pre></div><p>With access tokens in hand, I ran the tool and hit my second problem.</p>
<h3 id="tailscale-acls-bite-me-in-the-ass">Tailscale ACLs bite me in the ass</h3>
<p>The way Tailscale&rsquo;s networking shebang works meant that the server running this isolated Gitea service couldn&rsquo;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>
<h3 id="mirrors-of-github-private-repos-did-not-work">Mirrors of GitHub private repos did not work</h3>
<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&rsquo;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>
<p>When I ran the tool again it was able to successfully recreate all the repositories from my Gitea server.</p>
<h1 id="conclusion">Conclusion</h1>
<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>
]]></content:encoded>
    </item>
    
    <item>
      <title>Mildly overengineering my Glance configuration</title>
      <link>https://msfjarvis.dev/posts/mildly-overengineering-my-glance-configuration/</link>
      <pubDate>Wed, 07 May 2025 23:05:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/mildly-overengineering-my-glance-configuration/</guid>
      <description>The story of setting up a live environment for configuring my Glance dashboard</description>
      <content:encoded><![CDATA[<blockquote>
<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>
</blockquote>
<h1 id="setting-the-scene">Setting the scene</h1>
<p>I&rsquo;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>
<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>
<p>One way I&rsquo;ve seen this addressed (please message me on <code>@msfjarvis@androiddev.social</code> if you can find the actual blog post) is to have some sort of &ldquo;dev&rdquo; 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 &ldquo;dev&rdquo; mode will make the configuration go through the more usual Nix ways and give you your declarative system back.</p>
<p>I think that idea is cool! I also think that idea kinda sucks when the configuration is in a &ldquo;language&rdquo; you despise, like YAML. Which is where I am!</p>
<h1 id="coming-up-with-a-solution">Coming up with a solution</h1>
<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&rsquo;s break it down!</p>
<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>
<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>
<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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># This generates the following (incorrect) YAML</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># foo: &#34;baz&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">foo</span><span class="o">.</span><span class="n">bar</span> <span class="o">=</span> <span class="s2">&#34;baz&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># This way of declaraing the attrsets works around the</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># issue and generates the following:</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># foo:</span>
</span></span><span class="line"><span class="cl">  <span class="c1">#   bar: baz</span>
</span></span><span class="line"><span class="cl">  <span class="n">foo</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">bar</span> <span class="o">=</span> <span class="s2">&#34;baz&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Glance will automatically reload itself if the config file changes, which
greatly simplifies the setup. The only remaining bit we have is to automatically
generate this YAML every time we make a change to the Nix file, so that we can freely
switch between editor and web browser to see changes without taking a trip to the
terminal.</p>
<p>I solved this using <a href="https://github.com/watchexec/watchexec">watchexec</a> and a tiny Bash script that I stuffed
into a <a href="https://github.com/numtide/devshell">devshell</a> command:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">out_conf</span><span class="o">=</span><span class="k">$(</span>mktemp<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="si">${</span><span class="nv">lib</span><span class="p">.getExe pkgs.watchexec</span><span class="si">}</span> -r -w <span class="s2">&#34;</span><span class="si">${</span><span class="nv">glanceConf</span><span class="si">}</span><span class="s2">&#34;</span> -- <span class="s2">&#34;</span><span class="si">${</span><span class="nv">lib</span><span class="p">.getExe pkgs.nix-converter</span><span class="si">}</span><span class="s2"> --from-nix -f </span><span class="si">${</span><span class="nv">glanceConf</span><span class="si">}</span><span class="s2"> -l yaml &gt; </span><span class="nv">$out_conf</span><span class="s2">&#34;</span> <span class="p">&amp;</span>
</span></span><span class="line"><span class="cl"><span class="si">${</span><span class="nv">lib</span><span class="p">.getExe pkgs.glance</span><span class="si">}</span> -config <span class="s2">&#34;</span><span class="nv">$out_conf</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p>This is a mix of regular Bash and Nix which will make sense to Nix users, but
for everyone else: <code>${lib.getExe pkgs.watchexec}</code> gives you a path of the form
<code>/nix/store/store-hash-watchexec-version/bin/watchexec</code> which is the binary you
can execute. <code>$glanceConf</code> is a Nix variable containing a string path to the
<code>glance.nix</code> file we pulled out earlier.</p>
<p>All put together it looks like
<a href="https://msfjarvis.dev/g/dotfiles/e1bffa7e9d97">this</a>. If I need to edit my
Glance config, I will go into the <code>glance</code> devShell, boot up
<a href="https://zed.dev">Zed</a>, run the <code>dev</code> command in my terminal and enjoy a fully
automated iteration experience.</p>
<h2 id="caveats">Caveats</h2>
<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>
<h2 id="may-10th-update">May 10th update</h2>
<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>
<p>The bulk of the work is now handled by a bash script, which I have annotated as comments below to explain what&rsquo;s going on.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># EXPR is a Nix expression that we will be evaluating </span>
</span></span><span class="line"><span class="cl"><span class="c1"># and building to obtain our final YAML</span>
</span></span><span class="line"><span class="cl"><span class="nv">EXPR</span><span class="o">=</span><span class="s2">&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">let
</span></span></span><span class="line"><span class="cl"><span class="s2">  # Load nixpkgs from the search path because it&#39;s the easiest way to do it.
</span></span></span><span class="line"><span class="cl"><span class="s2">  pkgs = import &lt;nixpkgs&gt; { };
</span></span></span><span class="line"><span class="cl"><span class="s2">  # This is the same code we use in our Glance module to generate the YAML
</span></span></span><span class="line"><span class="cl"><span class="s2">  settingsFormat = pkgs.formats.yaml { };
</span></span></span><span class="line"><span class="cl"><span class="s2">  settingsFile = settingsFormat.generate \&#34;glance.yaml\&#34; (import ./</span><span class="si">${</span><span class="nv">1</span><span class="p">:?</span><span class="si">}</span><span class="s2">);
</span></span></span><span class="line"><span class="cl"><span class="s2">in
</span></span></span><span class="line"><span class="cl"><span class="s2"># settingsFile returns a derivation that the `nix build` below will, well, build.
</span></span></span><span class="line"><span class="cl"><span class="s2">\&#34;\${settingsFile}\&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">nix build <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="c1"># Prevent Nix from trying to check binary caches each time we build</span>
</span></span><span class="line"><span class="cl">  --option substitute <span class="nb">false</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="c1"># Feed the nixpkgs input from your flakes registry to the search path</span>
</span></span><span class="line"><span class="cl">  --impure -I <span class="nv">nixpkgs</span><span class="o">=</span>flake:nixpkgs <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="c1"># Pass in our expression from above that will emit the derivation to be built</span>
</span></span><span class="line"><span class="cl">  --expr <span class="s2">&#34;</span><span class="si">${</span><span class="nv">EXPR</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ./result is the default nix3-build output path</span>
</span></span><span class="line"><span class="cl">glance -config ./result
</span></span></code></pre></div>]]></content:encoded>
    </item>
    
    <item>
      <title>Migrating from Simple Analytics to self-hosted Plausible</title>
      <link>https://msfjarvis.dev/posts/migrating-from-simple-analytics-to-self-hosted-plausible/</link>
      <pubDate>Tue, 22 Apr 2025 16:43:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/migrating-from-simple-analytics-to-self-hosted-plausible/</guid>
      <description>Documenting the misadventures of self-hosting my site analytics</description>
      <content:encoded><![CDATA[<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&rsquo;s big bundle of freemium services provided to students and they just didn&rsquo;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>
<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>
<h1 id="choosing-the-analytics-service">Choosing the analytics service</h1>
<p>Like the absolute chump I am, my first visit was to <a href="https://awesome-selfhosted.net/tags/analytics.html">awesome-selfhosted&rsquo;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>
<p>I will be honest here, the reason to not choose Matomo was very superficial - I didn&rsquo;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>
<h1 id="getting-set-up">Getting set up</h1>
<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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="n">services</span><span class="o">.</span><span class="n">caddy</span><span class="o">.</span><span class="n">virtualHosts</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;https://stats.msfjarvis.dev&#34;</span> <span class="o">=</span> <span class="k">let</span>
</span></span><span class="line"><span class="cl">    <span class="n">p</span> <span class="o">=</span> <span class="n">config</span><span class="o">.</span><span class="n">services</span><span class="o">.</span><span class="n">plausible</span><span class="o">.</span><span class="n">server</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">in</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">extraConfig</span> <span class="o">=</span> <span class="s1">&#39;&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">      reverse_proxy </span><span class="si">${</span><span class="n">p</span><span class="o">.</span><span class="n">listenAddress</span><span class="si">}</span><span class="s1">:</span><span class="si">${</span><span class="nb">toString</span> <span class="n">p</span><span class="o">.</span><span class="n">port</span><span class="si">}</span><span class="s1">
</span></span></span><span class="line"><span class="cl"><span class="s1">    &#39;&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sops</span><span class="o">.</span><span class="n">secrets</span><span class="o">.</span><span class="n">plausible-secret</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">sopsFile</span> <span class="o">=</span> <span class="n">lib</span><span class="o">.</span><span class="n">snowfall</span><span class="o">.</span><span class="n">fs</span><span class="o">.</span><span class="n">get-file</span> <span class="s2">&#34;secrets/plausible.yaml&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="n">services</span><span class="o">.</span><span class="n">plausible</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">enable</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">server</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">baseUrl</span> <span class="o">=</span> <span class="s2">&#34;https://stats.msfjarvis.dev&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">secretKeybaseFile</span> <span class="o">=</span> <span class="n">config</span><span class="o">.</span><span class="n">sops</span><span class="o">.</span><span class="n">secrets</span><span class="o">.</span><span class="n">plausible-secret</span><span class="o">.</span><span class="n">path</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span></code></pre></div><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>
<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&rsquo;s a net improvement to user privacy that I was too naive to think about in 2020.</p>
<h1 id="migrating-existing-data-from-simple-analytics">Migrating existing data from Simple Analytics</h1>
<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&rsquo;t let me just fill in the date manually.</p>
<p>Once I had the 45 mB CSV in hand, I looked around for someone else&rsquo;s homework to copy so I didn&rsquo;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>
<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>
<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>
<blockquote>
<p>I have a big gripe with Plausible&rsquo;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&rsquo;s in the <code>errors</code> column in the <code>oban_jobs</code> table of the <code>plausible</code> database.</p>
</blockquote>
<p>The final step was making the data public like it was on Simple Analytics, so my humble blog&rsquo;s popularity or lack thereof can continue to be <a href="https://stats.msfjarvis.dev/msfjarvis.dev">scrutinized by anyone</a>.</p>
<h1 id="wrapping-up">Wrapping up</h1>
<p>All things considered this took me approximately an hour to get done, most of which was spent digging through Plausible&rsquo;s GitHub issues and re-learning how to use the <code>psql</code> CLI. I&rsquo;m quite pleased with how it turned out in the end!</p>
<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>
]]></content:encoded>
    </item>
    
    <item>
      <title>Overengineering an Obsidian dashboard to get better at Marvel Snap</title>
      <link>https://msfjarvis.dev/posts/overengineering-an-obsidian-dashboard-to-get-better-at-marvel-snap/</link>
      <pubDate>Tue, 01 Apr 2025 10:46:52 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/overengineering-an-obsidian-dashboard-to-get-better-at-marvel-snap/</guid>
      <description>Using data to answer the ultimate gamer question - why am I so bad?</description>
      <content:encoded><![CDATA[<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
at least 2 of 3 randomly generated locations on the board in 6 turns.</p>
<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
to determine which player wins that location. You start at 1 energy and gain +1 energy every turn.</p>
<p>The game has a competitive mode, and I am horrendous at it. Over the course of the 2 years or so I&rsquo;ve played this game I&rsquo;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>
<p>Last month I <a href="/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>
<h1 id="laying-down-the-foundation">Laying down the foundation</h1>
<p>To get started with analysis, I first need to figure out what data to collect and how.</p>
<p>For a typical game, the stuff you want to record is the following:</p>
<ul>
<li>Your deck: The game has many archetypes, some more reliably effective than others</li>
<li>Opponent&rsquo;s deck: If they are playing a deck that hard counters or just tends to always beat yours</li>
<li>Locations: Some locations benefit certain decks and can heavily influence the result</li>
<li>Outcome: Whether you won, lost or retreated</li>
<li>Cubes: You lose or gain cubes depending on the outcome and the number of players who &ldquo;snapped&rdquo;</li>
</ul>
<p>Taking all this into account I settled on the following template that I would use for each individual game I recorded.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">date: {{date}}
</span></span><span class="line"><span class="cl">time: {{time}}
</span></span><span class="line"><span class="cl">type: snap-game
</span></span><span class="line"><span class="cl">result: win/loss/retreat
</span></span><span class="line"><span class="cl">opponent_name:
</span></span><span class="line"><span class="cl">opponent_deck:
</span></span><span class="line"><span class="cl">my_deck:
</span></span><span class="line"><span class="cl">locations:
</span></span><span class="line"><span class="cl">  <span class="k">-</span> location1
</span></span><span class="line"><span class="cl">  <span class="k">-</span> location2
</span></span><span class="line"><span class="cl">  <span class="k">-</span> location3
</span></span><span class="line"><span class="cl">cubes: # Use positive for gains (e.g., +4), negative for losses (e.g., -2)
</span></span><span class="line"><span class="cl">notes:
</span></span><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Game Details
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Key Moments
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Lessons Learned
</span></span></span></code></pre></div><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>
<p>Obsidian provides a very serviceable GUI to edit frontmatter fields which makes the job of filling it in pretty easy.</p>
<figure>
    <img loading="lazy" src="template.webp"
         alt="Obsidian&#39;s GUI for the frontmatter given above, of note is the date picker and the &#39;chips&#39; pattern for entering list values for the locations field"/> 
</figure>

<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>
<h1 id="building-the-visualization">Building the visualization</h1>
<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>
<p>As an example, here&rsquo;s the code for the dashboard that is in my daily note template to see the current day&rsquo;s games.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">TABLE</span><span class="w"> </span><span class="k">WITHOUT</span><span class="w"> </span><span class="n">ID</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">length</span><span class="p">(</span><span class="k">rows</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="s2">&#34;Total Games&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">length</span><span class="p">(</span><span class="n">filter</span><span class="p">(</span><span class="k">rows</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="n">x</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">x</span><span class="p">.</span><span class="k">result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&#34;win&#34;</span><span class="p">))</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">Wins</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">length</span><span class="p">(</span><span class="n">filter</span><span class="p">(</span><span class="k">rows</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="n">x</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">x</span><span class="p">.</span><span class="k">result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&#34;loss&#34;</span><span class="p">))</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">Losses</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">length</span><span class="p">(</span><span class="n">filter</span><span class="p">(</span><span class="k">rows</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="n">x</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">x</span><span class="p">.</span><span class="k">result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&#34;retreat&#34;</span><span class="p">))</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">Retreats</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">sum</span><span class="p">(</span><span class="k">map</span><span class="p">(</span><span class="k">rows</span><span class="p">.</span><span class="n">cubes</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="n">x</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="nb">number</span><span class="p">(</span><span class="n">x</span><span class="p">)))</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="s2">&#34;Net Cubes&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s2">&#34;snap-games&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&#34;snap-game&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">AND</span><span class="w"> </span><span class="nb">date</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">date</span><span class="p">(</span><span class="s2">&#34;{{date}}&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">FLATTEN</span><span class="w"> </span><span class="n">file</span><span class="p">.</span><span class="n">link</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">FileLink</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">null</span><span class="w">
</span></span></span></code></pre></div><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&rsquo;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>
<figure>
    <img loading="lazy" src="daily-dashboard.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"/> 
</figure>

<p>This can also be extended to cover the past week</p>
<figure>
    <img loading="lazy" src="weekly-dashboard.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"/> 
</figure>


<details>
  <summary>DataView code</summary>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">TABLE</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nb">date</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nb">Date</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="n">time</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">Time</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">result</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="k">Result</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="n">my_deck</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="s2">&#34;My Deck&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="n">opponent_deck</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="s2">&#34;Opponent&#39;s Deck&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="n">cubes</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="s2">&#34;Cubes Δ&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s2">&#34;snap-games&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&#34;snap-game&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">AND</span><span class="w"> </span><span class="nb">date</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="nb">date</span><span class="p">(</span><span class="n">today</span><span class="p">)</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">dur</span><span class="p">(</span><span class="mi">7</span><span class="w"> </span><span class="n">days</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">SORT</span><span class="w"> </span><span class="nb">date</span><span class="w"> </span><span class="k">desc</span><span class="p">,</span><span class="w"> </span><span class="n">time</span><span class="w"> </span><span class="k">desc</span><span class="w">
</span></span></span></code></pre></div></details>

<hr />

<p>I can see what decks I played and how they performed relative to each other</p>
<figure>
    <img loading="lazy" src="deck-performance.webp"
         alt="A table showing the decks I played in each row and their respective game counts, win rate and net cubes statistics"/> 
</figure>


<details>
  <summary>DataView code</summary>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">TABLE</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">length</span><span class="p">(</span><span class="k">rows</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">Games</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="n">round</span><span class="p">(</span><span class="k">length</span><span class="p">(</span><span class="n">filter</span><span class="p">(</span><span class="k">rows</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="n">x</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">x</span><span class="p">.</span><span class="k">result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&#34;win&#34;</span><span class="p">))</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="k">length</span><span class="p">(</span><span class="k">rows</span><span class="p">)</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">&#34;%&#34;</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="s2">&#34;Win Rate&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">sum</span><span class="p">(</span><span class="k">map</span><span class="p">(</span><span class="k">rows</span><span class="p">.</span><span class="n">cubes</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="n">x</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="nb">number</span><span class="p">(</span><span class="n">x</span><span class="p">)))</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="s2">&#34;Net Cubes&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s2">&#34;snap-games&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&#34;snap-game&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">AND</span><span class="w"> </span><span class="nb">date</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="nb">date</span><span class="p">(</span><span class="n">today</span><span class="p">)</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">dur</span><span class="p">(</span><span class="mi">7</span><span class="w"> </span><span class="n">days</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">my_deck</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">SORT</span><span class="w"> </span><span class="k">length</span><span class="p">(</span><span class="k">rows</span><span class="p">)</span><span class="w"> </span><span class="k">desc</span><span class="w">
</span></span></span></code></pre></div></details>

<hr />

<p>I can also see which locations favor me the most</p>
<figure>
    <img loading="lazy" src="locations.webp"
         alt="A table of every location with its name, number of times I played there, my win rate and the net cubes"/> 
</figure>


<details>
  <summary>DataView code</summary>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">TABLE</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">length</span><span class="p">(</span><span class="k">rows</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="s2">&#34;Times Encountered&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="n">round</span><span class="p">(</span><span class="k">length</span><span class="p">(</span><span class="n">filter</span><span class="p">(</span><span class="k">rows</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="n">x</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">x</span><span class="p">.</span><span class="k">result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&#34;win&#34;</span><span class="p">))</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="k">length</span><span class="p">(</span><span class="k">rows</span><span class="p">)</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">&#34;%&#34;</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="s2">&#34;Win Rate&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">sum</span><span class="p">(</span><span class="k">map</span><span class="p">(</span><span class="k">rows</span><span class="p">.</span><span class="n">cubes</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="n">x</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="nb">number</span><span class="p">(</span><span class="n">x</span><span class="p">)))</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="s2">&#34;Net Cubes&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s2">&#34;snap-games&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">&#34;snap-game&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">AND</span><span class="w"> </span><span class="nb">date</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="nb">date</span><span class="p">(</span><span class="n">today</span><span class="p">)</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">dur</span><span class="p">(</span><span class="mi">7</span><span class="w"> </span><span class="n">days</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">FLATTEN</span><span class="w"> </span><span class="n">locations</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">locations</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">SORT</span><span class="w"> </span><span class="k">sum</span><span class="p">(</span><span class="k">map</span><span class="p">(</span><span class="k">rows</span><span class="p">.</span><span class="n">cubes</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="n">x</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="nb">number</span><span class="p">(</span><span class="n">x</span><span class="p">)))</span><span class="w"> </span><span class="k">desc</span><span class="w">
</span></span></span></code></pre></div></details>

<h1 id="did-it-help">Did it help?</h1>
<p>I don&rsquo;t know!</p>
<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>
<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&rsquo;s a very small sample size. The new season of Snap will start tomorrow so hopefully I&rsquo;ll be able to get back into it and actually figure out if this whole shebang was worth it.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>The Obsidian Migration - One Week Later</title>
      <link>https://msfjarvis.dev/posts/the-obsidian-migration--one-week-later/</link>
      <pubDate>Mon, 17 Mar 2025 11:08:37 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/the-obsidian-migration--one-week-later/</guid>
      <description>An experience report of using Obsidian for a week, coming from a year of Logseq</description>
      <content:encoded><![CDATA[<p>In my <a href="/posts/migrating-from-logseq-to-obsidian">last post</a> I had a list of to-dos to fully finish up the migration, which I&rsquo;ll reproduce below:</p>
<blockquote>
<ul>
<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&rsquo;s tags mechanism.</li>
<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>
<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>
</ul>
</blockquote>
<h2 id="migrating-dates-and-tags-to-obsidian">Migrating dates and tags to Obsidian</h2>
<p>I haven&rsquo;t found a solution for automated tagging yet, but inspired by Simon Willison&rsquo;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>
<p>The choice of Python was for multiple reasons: it&rsquo;s fast enough for the job, I understand it well, and it can get much farther without needing third-party dependencies.</p>
<p>There wasn&rsquo;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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">Write a Python script using only the standard library to process an Obsidian
</span></span><span class="line"><span class="cl">vault and migrate Logseq patterns to Obsidian&#39;s systems in the markdown files.
</span></span><span class="line"><span class="cl">Below is a list of changes to make:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">1. Convert any `[[Thing]]` style links to use Obsidian-native tags by adding a
</span></span><span class="line"><span class="cl">`#` prefix, resulting in `#Thing`. If there are spaces in the link text,
</span></span><span class="line"><span class="cl">convert them to underscores.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">2. If a link&#39;s text matches the date format `May 8th, 2024`, replace its text
</span></span><span class="line"><span class="cl">by changing the date format to YYYY-MM-DD. In the given example, the output
</span></span><span class="line"><span class="cl">should be `[[2024-05-08]]`.
</span></span></code></pre></div><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>
<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&rsquo;m not sure if I would&rsquo;ve figured that one out right away, but it was a good way to also learn more about the Python standard library&rsquo;s date formatting facilities.</p>
<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>
<h2 id="getting-set-up-on-mobile">Getting set up on mobile</h2>
<p>This was the bit I was more excited for, because one major advantage of using Obsidian on mobile that I didn&rsquo;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&rsquo;re running.</p>
<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://androiddev.social/@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>
<p>Once I had removed Logseq and imported my existing vault into Obsidian (I&rsquo;ve opted to re-use my existing Git repository), I followed PuppyGit&rsquo;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>
<p>The Git plugin I was using for my desktop didn&rsquo;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>
<hr />

<p>A week into it I find myself much happier using Obsidian than I ever was with Logseq, and I&rsquo;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>
]]></content:encoded>
    </item>
    
    <item>
      <title>Migrating from Logseq to Obsidian</title>
      <link>https://msfjarvis.dev/posts/migrating-from-logseq-to-obsidian/</link>
      <pubDate>Sun, 09 Mar 2025 15:59:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/migrating-from-logseq-to-obsidian/</guid>
      <description>Logseq&amp;rsquo;s clunky apps and glacial development pace finally motivated me to migrate my journaling to Obsidian, here&amp;rsquo;s how I did it.</description>
      <content:encoded><![CDATA[<p>I have been using <a href="https://logseq.com">Logseq</a> for just over <a href="https://androiddev.social/@msfjarvis/112378523734491769">a year</a> to maintain a daily journal, but I&rsquo;ve been eyeing Obsidian for the last couple months as my note-keeping needs have expanded past daily journaling.</p>
<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>
<h2 id="moving-over-content">Moving over content</h2>
<p>I made the following changes to the files in my Logseq graph when moving to the new Obsidian Vault</p>
<ul>
<li><code>assets</code> and <code>pages</code> got copied over as-is</li>
<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>
</ul>
<h2 id="plugins">Plugins</h2>
<p>I enabled the following core plugins:</p>
<ul>
<li><strong>Daily Notes</strong>: Automates creation of daily notes</li>
<li><strong>Templates</strong>: Allows you to create reusable templates</li>
<li><strong>Backlinks</strong>: Shows references to the current note</li>
</ul>
<p>Looking around other people&rsquo;s experience with this migration path I also found the following community plugins:</p>
<ul>
<li><strong>Periodic Notes</strong>: Enhanced daily/weekly/monthly notes</li>
<li><strong>Natural Language Dates</strong>: Type dates like &ldquo;tomorrow&rdquo; or &ldquo;next Monday&rdquo;</li>
<li><strong>Tasks</strong>: Advanced task management</li>
<li><strong>Dataview</strong>: Query and display info from your notes</li>
<li><strong>Templater</strong>: More powerful templating than the core plugin</li>
<li><strong>Git</strong>: Self-managed sync for vaults</li>
</ul>
<blockquote>
<p>Side note: I greatly appreciate the fact that Obsidian defaults to vendoring plugins in the Vault itself so if you&rsquo;re tracking the <code>.obsidian</code> folder in Git, you get a fully reproducible Vault regardless of what machine you open it on.</p>
</blockquote>
<h2 id="configuration-changes">Configuration changes</h2>
<p>In Settings → Daily Notes:</p>
<ul>
<li><strong>Date format</strong>: <code>YYYY-MM-DD</code></li>
<li><strong>New file location</strong>: <code>Daily/</code></li>
<li><strong>Open daily note on startup</strong>: <code>Enabled</code></li>
</ul>
<p>In Settings → Hotkeys:</p>
<ul>
<li>Open today&rsquo;s daily note: <code>Alt+D</code></li>
<li>Create new note: <code>Ctrl+N</code></li>
<li>Toggle edit/preview mode: <code>Ctrl+E</code></li>
<li>Search in all notes: <code>Ctrl+Shift+F</code></li>
</ul>
<h1 id="pending-changes">Pending changes</h1>
<p>What I&rsquo;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>
<ul>
<li>
<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&rsquo;s tags mechanism.</p>
</li>
<li>
<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>
</li>
<li>
<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>
</li>
</ul>
<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://androiddev.social/@msfjarvis/114132263177269990">Mastodon</a> or the comments here if you have any tips to improve things.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Assorted NixOS things</title>
      <link>https://msfjarvis.dev/posts/assorted-nixos-things/</link>
      <pubDate>Sat, 07 Dec 2024 20:19:00 +0000</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/assorted-nixos-things/</guid>
      <description>Running log of random things I&amp;rsquo;ve learned about Nix/NixOS/Nixpkgs</description>
      <content:encoded><![CDATA[<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&rsquo;ll attempt to keep up-to-date with my personal Logseq graph where I&rsquo;ve kept the record of them.</p>
<h1 id="nix3-flake-metadata">nix3-flake-metadata</h1>
<p>The &ldquo;new&rdquo; (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">&ldquo;1000 instances of Nixpkgs&rdquo;</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>
<h1 id="nixos-module-system">NixOS module system</h1>
<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>
<h1 id="updating-your-custom-packages-from-a-git-branch">Updating your custom packages from a Git branch</h1>
<blockquote>
<p>This isn&rsquo;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>
</blockquote>
<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>
<h1 id="the-nix-language">The Nix language</h1>
<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 &quot;field&quot; list</code>.</p>
<h1 id="building-packages-from-a-repl">Building packages from a REPL</h1>
<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&rsquo;s instance of Nixpkgs.</p>
<h1 id="avoiding-mysterious-errors-with-fixed-output-derivations">Avoiding mysterious errors with fixed-output derivations</h1>
<p>For many languages Nixpkgs&rsquo; builders vendor dependencies in a fixed-output derivation (FOD) which will give seemingly random errors if the contents change but its name doesn&rsquo;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>
<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>
<h1 id="tailscale-exit-nodes-on-nixos">Tailscale exit nodes on NixOS</h1>
<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>
<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>
<h1 id="dual-booting-windows-using-systemd-boot">Dual booting Windows using systemd-boot</h1>
<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>
<h1 id="nixs-builtinstostring">Nix&rsquo;s <code>builtins.toString</code></h1>
<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&rsquo;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>
]]></content:encoded>
    </item>
    
    <item>
      <title>Deploying applications to Fly.io without Docker</title>
      <link>https://msfjarvis.dev/posts/deploying-applications-to-flyio-without-docker/</link>
      <pubDate>Sat, 05 Oct 2024 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/deploying-applications-to-flyio-without-docker/</guid>
      <description>A quick guide to deploying apps to Fly.io without a local Docker installation</description>
      <content:encoded><![CDATA[<blockquote>
<p>This post assumes basic familiarity with Fly.io, the flyctl CLI and Nix.</p>
</blockquote>
<hr />

<p><a href="https://fly.io">Fly.io</a> is one of the many players in the &ldquo;We run your app close to your users&rdquo; space. I&rsquo;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>
<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>
<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>
<h2 id="setting-up">Setting up</h2>
<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&rsquo;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>
<p>You&rsquo;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>
<h2 id="building-and-deploying">Building and deploying</h2>
<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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">➜ ls -l result
</span></span><span class="line"><span class="cl">result ⇒ /nix/store/h0jgv00fg68d7gsaadd11ydpk6wxxn8w-docker-image-linkleaner.tar.gz
</span></span></code></pre></div><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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">skopeo <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="c1"># Remove the need for a policy file since we only need this for a one off thing</span>
</span></span><span class="line"><span class="cl">  --insecure-policy <span class="se">\
</span></span></span><span class="line"><span class="cl">  copy <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="c1"># The source argument, which is a docker image archive at `./result`</span>
</span></span><span class="line"><span class="cl">  docker-archive:./result <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="c1"># The destination argument, the `build.image` value from your fly.toml</span>
</span></span><span class="line"><span class="cl">  docker://registry.fly.io/linkleaner:latest <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="c1"># Injecting the previously generated credentials</span>
</span></span><span class="line"><span class="cl">  --dest-creds x:<span class="s2">&#34;</span><span class="nv">$FLY_AUTH_TOKEN</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="c1"># Docker v2.2 manifest format: https://containers.gitbook.io/build-containers-the-hard-way#registry-format-oci-image-manifest</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Probably not necessary but I just wanted to be sure</span>
</span></span><span class="line"><span class="cl">  --format v2s2
</span></span></code></pre></div><p>This will print a bunch of stuff while it&rsquo;s doing its thing and upon success you have now placed your organically built, Docker-free OCI image into the Fly.io registry.</p>
<hr />

<p>And that&rsquo;s more or less it! You can now proceed with the usual Fly.io deployment process of running <code>flyctl deploy</code> and it&rsquo;ll be able to find the image and run your application:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">➜ flyctl <span class="nv">deploy</span>
</span></span><span class="line"><span class="cl"><span class="o">==</span>&gt; Verifying app config
</span></span><span class="line"><span class="cl">Validating /home/msfjarvis/code/linkleaner/fly.toml
</span></span><span class="line"><span class="cl">✓ Configuration is valid
</span></span><span class="line"><span class="cl">--&gt; Verified app <span class="nv">config</span>
</span></span><span class="line"><span class="cl"><span class="o">==</span>&gt; Building image
</span></span><span class="line"><span class="cl">Searching <span class="k">for</span> image <span class="s1">&#39;registry.fly.io/linkleaner:latest&#39;</span> remotely...
</span></span><span class="line"><span class="cl">image found: img_3mno4wk809624k18
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Watch your deployment at https://fly.io/apps/linkleaner/monitoring
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">-------
</span></span><span class="line"><span class="cl">Updating existing machines in <span class="s1">&#39;linkleaner&#39;</span> with rolling strategy
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">-------
</span></span><span class="line"><span class="cl"> ✔ <span class="o">[</span>1/2<span class="o">]</span> Cleared lease <span class="k">for</span> 0801937b607358
</span></span><span class="line"><span class="cl"> ✔ <span class="o">[</span>2/2<span class="o">]</span> Cleared lease <span class="k">for</span> 9080155dfe0de8
</span></span><span class="line"><span class="cl">-------
</span></span></code></pre></div>]]></content:encoded>
    </item>
    
    <item>
      <title>Assorted Git things</title>
      <link>https://msfjarvis.dev/posts/assorted-git-things/</link>
      <pubDate>Tue, 01 Oct 2024 13:57:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/assorted-git-things/</guid>
      <description>A running log of Git concepts I&amp;rsquo;ve learned since I started journaling</description>
      <content:encoded><![CDATA[<p>When I asked for suggestions about note-taking apps <a href="https://androiddev.social/@msfjarvis/112378523734491769">back in May</a> I wasn&rsquo;t fully convinced I&rsquo;d be able to stick with it, but nearly 6 months later it has probably been the thing I&rsquo;ve been most consistent about. Anyways, here&rsquo;s a rather short list of Git things off the <a href="https://dictionary.cambridge.org/us/dictionary/english/til">&lsquo;TIL&rsquo;</a> list in my Logseq graph. I&rsquo;ll keep adding new things to the top as they come up.</p>
<h1 id="git_editor-variable">GIT_EDITOR variable</h1>
<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>
<h1 id="git-add"><code>git add</code></h1>
<ul>
<li><code>git add -u</code> will stage changes to all tracked files and leave untracked ones alone.</li>
<li><code>git add -N</code> will stage a file without any of its changes. You can treat that as an &ldquo;intention&rdquo; 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>
</ul>
<h1 id="git-submodules">Git submodules</h1>
<p>Probably the biggest pain in the ass in all of Git land. Every time I&rsquo;ve had to use a submodule, I&rsquo;ve disliked the experience. Here&rsquo;s how to <em>properly</em> get rid of a submodule in case you&rsquo;re switching to <a href="https://github.com/ingydotnet/git-subrepo">git-subrepo</a> or a different solution.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl">git submodule deinit -f path/to/module
</span></span><span class="line"><span class="cl">rm -rf .git/modules/path/to/module
</span></span><span class="line"><span class="cl">git config -f .gitmodules --remove-section submodule.path/to/module
</span></span><span class="line"><span class="cl">git add .gitmodules
</span></span><span class="line"><span class="cl">git rm --cached path/to/module
</span></span></code></pre></div><p>In order, this does the following:</p>
<ol>
<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>
<li>Deletes the bare repository Git creates of the submodule which is used to generate the actual checkout</li>
<li>Edits your <code>.gitmodules</code> file to remove the submodule entry</li>
<li>Stages the changes to <code>.gitmodules</code></li>
<li>Deletes the submodule fully from the index</li>
</ol>
<p>At this point you can commit the changes and hopefully never deal with a submodule again :D</p>
<h1 id="git-credential-helpers">Git credential helpers</h1>
<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>
<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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl"># for URL https://git.msfjarvis.dev/msfjarvis/super-private
</span></span><span class="line"><span class="cl">$ pass-git-helper get
</span></span><span class="line"><span class="cl">&lt; protocol=https
</span></span><span class="line"><span class="cl">&lt; host=git.msfjarvis.dev
</span></span><span class="line"><span class="cl">&lt; path=msfjarvis/super-private
</span></span><span class="line"><span class="cl">&gt; protocol=https
</span></span><span class="line"><span class="cl">&gt; host=git.msfjarvis.dev
</span></span><span class="line"><span class="cl">&gt; username=msfjarvis
</span></span><span class="line"><span class="cl">&gt; password=hunter2
</span></span></code></pre></div><h1 id="maintaining-a-fork-in-the-presence-of-fetchprunetags">Maintaining a fork in the presence of <code>fetch.pruneTags</code></h1>
<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>
<p>One solution suggested for this was setting <code>remotes.&lt;name&gt;.tagOpt = &quot;--no-tags&quot;</code> but that didn&rsquo;t do the job for me.</p>
<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>
]]></content:encoded>
    </item>
    
    <item>
      <title>A tour of my screenshots folder</title>
      <link>https://msfjarvis.dev/posts/a-tour-of-my-screenshots-folder/</link>
      <pubDate>Mon, 01 Apr 2024 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/a-tour-of-my-screenshots-folder/</guid>
      <description>The history of my Minecraft adventures as told by my screenshots folder</description>
      <content:encoded><![CDATA[<h2 id="preface">Preface</h2>
<blockquote>
<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&rsquo;t normally do.</p>
</blockquote>
<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&rsquo;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>
<h2 id="just-a-bridge-really">Just a bridge, really</h2>
<figure>
    <img loading="lazy" src="starter-base-bridge.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."/> <figcaption>
            Date taken: August 2, 2022
        </figcaption>
</figure>

<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>
<h2 id="an-unlikely-friendship">An unlikely friendship</h2>
<figure>
    <img loading="lazy" src="an-unlikely-friendship.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."/> <figcaption>
            Date taken: August 13, 2022
        </figcaption>
</figure>

<p>I don&rsquo;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>
<h2 id="the-start-of-the-storage-room">The start of the storage room</h2>
<figure>
    <img loading="lazy" src="the-start-of-the-storage-room.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"/> <figcaption>
            Date taken: August 20, 2022
        </figcaption>
</figure>

<p>After commandeering the aforemention 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>
<h2 id="i-am-a-dwarf-and-im-digging-a-hole">I am a dwarf, and I&rsquo;m digging a hole</h2>
<figure>
    <img loading="lazy" src="diggy-diggy-hole.webp"
         alt="A top down shot of my Minecraft character standing next to a one chunk big hole straight down to bedrock"/> <figcaption>
            Date taken: December 29, 2022
        </figcaption>
</figure>

<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>
<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>
<figure>
    <img loading="lazy" src="draining-ocean-monument-part-1.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"/> <figcaption>
            Date taken: February 27, 2023
        </figcaption>
</figure>

<p>I didn&rsquo;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&rsquo;ll let the other screenshots paint the picture, but suffice to say I had under-estimated the scope of this 😬</p>
<p><figure>
    <img loading="lazy" src="draining-ocean-monument-part-2.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."/> <figcaption>
            Date taken: March 7, 2023
        </figcaption>
</figure>

<figure>
    <img loading="lazy" src="draining-ocean-monument-part-3.webp"
         alt="The same setup described before but with about 55% of the structure drained."/> <figcaption>
            Date taken: March 12, 2023
        </figcaption>
</figure>

<figure>
    <img loading="lazy" src="draining-ocean-monument-part-4.webp"
         alt="The entire structure is now drained, with just the sand perimeter remaining around it"/> <figcaption>
            Date taken: March 15, 2023
        </figcaption>
</figure>

<figure>
    <img loading="lazy" src="draining-ocean-monument-part-5.webp"
         alt="Me standing on top of one of the sand walls, looking inwards to the now completed Guardian farm. It&#39;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"/> <figcaption>
            Date taken: March 20, 2023
        </figcaption>
</figure>
</p>
<p>When this was finally done I genuinely used it like 4 times total, turns out I&rsquo;m not really a prismarine guy so that&rsquo;s a couple weeks I am not getting back.</p>
<h2 id="getting-real-personal-with-a-warden">Getting real personal with a Warden</h2>
<figure>
    <img loading="lazy" src="meeting-a-warden.webp"
         alt="A very close over the shoulder shot of me mere inches from a Warden which is staring right into my soul"/> <figcaption>
            Date taken: March 12, 2023
        </figcaption>
</figure>

<p>To break up the monotony of placing sand for the Guardian farm I paid a visit to a near by Ancient City and ended up a little too close to a Warden, which did eventually kill me.</p>
<h2 id="the-sea-shanty-era">The sea shanty era</h2>
<figure>
    <img loading="lazy" src="tragic-lovers.webp"
         alt="Two players in a bamboo raft that is positioned on the bow of a shipwreck which is poking out of water"/> <figcaption>
            Date taken: June 27, 2023
        </figcaption>
</figure>

<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&rsquo;s interest dwindled down and I shut the server down a couple weeks later.</p>
<h2 id="excavating-the-nether">Excavating the Nether</h2>
<figure>
    <img loading="lazy" src="nether-mining-pre-boom.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"/> <figcaption>
            Date taken: July 11, 2023
        </figcaption>
</figure>

<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&rsquo;s the damage all the TNT did:</p>
<figure>
    <img loading="lazy" src="nether-mining-post-boom.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"/> <figcaption>
            Date taken: July 11, 2023
        </figcaption>
</figure>

<h2 id="my-first-sniffers">My first Sniffers</h2>
<figure>
    <img loading="lazy" src="first-sniffers.webp"
         alt="A fenced off patch of moss with me standing in the middle and one Sniffer egg on each side"/> <figcaption>
            Date taken: July 12, 2023
        </figcaption>
</figure>

<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 &ldquo;sniff&rdquo; up.</p>
<figure>
    <img loading="lazy" src="sniffer-for-a-sniffer-farm.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"/> <figcaption>
            Date taken: September 13, 2023
        </figcaption>
</figure>

<h2 id="the-end-ring-project">The End Ring Project</h2>
<figure>
    <img loading="lazy" src="incomplete-end-island-ring.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"/> <figcaption>
            Date taken: March 28, 2024
        </figcaption>
</figure>

<p>I envisioned a continous 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 &ldquo;circle&rdquo; so many times that it&rsquo;s kinda stalled at the moment.</p>
<h2 id="all-trimmed-up">All Trimmed Up</h2>
<figure>
    <img loading="lazy" src="obtaining-every-armor-trim.webp"
         alt="A short hallway of Spruce planks showcasing every single Armor Trim as of Minecraft 1.20.4"/> <figcaption>
            Date taken: March 16, 2024
        </figcaption>
</figure>

<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>
<h2 id="bonus-randomness">Bonus randomness</h2>
<h3 id="the-minecraft-x-faze-collab">The Minecraft x FaZe collab</h3>
<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>
<p><figure>
    <img loading="lazy" src="fazeup-1.webp"
         alt="The FaZe Fossil in a soul sand valley"/> 
</figure>

<figure>
    <img loading="lazy" src="fazeup-2.webp"
         alt="Another FaZe Fossil in a soul sand valley"/> 
</figure>

<figure>
    <img loading="lazy" src="fazeup-3.webp"
         alt="Yet Another FaZe Fossil in a soul sand valley"/> 
</figure>

<figure>
    <img loading="lazy" src="fazeup-4.webp"
         alt="Would you believe it? Another FaZe logo in a soul sand valley"/> 
</figure>

<figure>
    <img loading="lazy" src="fazeup-5.webp"
         alt="To break up the monotony, this FaZe logo was in a &#39;Quartz Flats&#39; custom biome from the Incendium datapack"/> 
</figure>
</p>
<h3 id="the-impossible-portal">The impossible portal</h3>
<figure>
    <img loading="lazy" src="chunk-pruned-portal.webp"
         alt="A Nether portal with its left side of Obsidian blocks missing but the portal is still intact"/> <figcaption>
            Date taken: June 12, 2023
        </figcaption>
</figure>

<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&rsquo;s all the way back at my starter base so it does not see much use, but it&rsquo;s there.</p>
<h2 id="the-end">The end&hellip;?</h2>
<p>I don&rsquo;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>
<p>Hopefully I have something cooler for next year and that I actually give myself more than 4 hours to write it up.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Improving dependency sync speeds for your Gradle project</title>
      <link>https://msfjarvis.dev/posts/improving-dependency-sync-speeds-for-your-gradle-project/</link>
      <pubDate>Sun, 31 Mar 2024 03:13:07 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/improving-dependency-sync-speeds-for-your-gradle-project/</guid>
      <description>Waiting for Gradle to download dependencies is so 2023</description>
      <content:encoded><![CDATA[<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>
<h2 id="preface">Preface</h2>
<p>This post is going to assume some things about you and your project, but you should be fine even if these aren&rsquo;t true for you.</p>
<ul>
<li>You&rsquo;re somewhat comfortable mucking around with Gradle</li>
<li>Your project is using Gradle 8.7, the latest as of writing</li>
</ul>
<p>If you&rsquo;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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">- includeGroupAndSubgroups(&#34;com.example&#34;)
</span></span></span><span class="line"><span class="cl"><span class="gi">+ includeGroupByRegex(&#34;com.example.*&#34;)
</span></span></span></code></pre></div><h2 id="obtaining-a-baseline">Obtaining a baseline</h2>
<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&rsquo;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>
<p>In Android Studio, create a new run configuration for Gradle&rsquo;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>
<figure>
    <img loading="lazy" src="run-configuration.webp"/> <figcaption>
            The Android Studio Run configuration window configured with the task &#39;:android:dependencies --refresh-dependencies&#39;
        </figcaption>
</figure>

<p>When you run this task, you&rsquo;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>
<figure>
    <img loading="lazy" src="download-info.webp"/> <figcaption>
            Download info window showing a list of network requests and their sources while the task is running
        </figcaption>
</figure>

<h2 id="how-gradle-fetches-your-dependencies">How Gradle fetches your dependencies</h2>
<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&rsquo;s pretty jargon-heavy and too much of a detour from what we&rsquo;re really here to do so I&rsquo;ll just Spark Notes™️ it and move on to more fun stuff.</p>
<ul>
<li>Gradle requires declaring <strong>repositories</strong> where dependencies are fetched from.</li>
<li>Dependencies are looked up in each repository, <strong>in declaration order</strong>, until they are found in one.</li>
<li>Gradle makes a lot of network requests as part of this lookup, and it is in our interest to reduce them.</li>
</ul>
<h2 id="a-quick-attempt-at-optimisation">A quick attempt at optimisation</h2>
<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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="c1">// settings.gradle.kts
</span></span></span><span class="line"><span class="cl"><span class="n">pluginManagement</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">repositories</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">mavenCentral</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">google</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">dependencyResolutionManagement</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">repositories</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">maven</span><span class="p">(</span><span class="s2">&#34;https://jitpack.io&#34;</span><span class="p">)</span> <span class="p">{</span> <span class="n">name</span> <span class="p">=</span> <span class="s2">&#34;JitPack&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">google</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">mavenCentral</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">maven</span><span class="p">(</span><span class="s2">&#34;https://oss.sonatype.org/content/repositories/snapshots/&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">name</span> <span class="p">=</span> <span class="s2">&#34;Sonatype snapshots</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><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>
<p>For example, in a typical Android build most of your plugin classpath will be dominated by the Android Gradle Plugin (AGP), so you&rsquo;d want gMaven to be come first so that Gradle does not waste time trying to find AGP on Maven Central.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"> pluginManagement {
</span></span><span class="line"><span class="cl">   repositories {
</span></span><span class="line"><span class="cl"><span class="gd">-    mavenCentral()
</span></span></span><span class="line"><span class="cl">     google()
</span></span><span class="line"><span class="cl"><span class="gi">+    mavenCentral()
</span></span></span><span class="line"><span class="cl">   }
</span></span><span class="line"><span class="cl"> }
</span></span></code></pre></div><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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"> dependencyResolutionManagement {
</span></span><span class="line"><span class="cl">   repositories {
</span></span><span class="line"><span class="cl"><span class="gd">-    maven(&#34;https://jitpack.io&#34;) { name = &#34;JitPack&#34; }
</span></span></span><span class="line"><span class="cl">     google()
</span></span><span class="line"><span class="cl">     mavenCentral()
</span></span><span class="line"><span class="cl"><span class="gi">+    maven(&#34;https://jitpack.io&#34;) { name = &#34;JitPack&#34; }
</span></span></span><span class="line"><span class="cl">     maven(&#34;https://oss.sonatype.org/content/repositories/snapshots/&#34;) {
</span></span><span class="line"><span class="cl">       name = &#34;Sonatype snapshots
</span></span><span class="line"><span class="cl">     }
</span></span><span class="line"><span class="cl">   }
</span></span><span class="line"><span class="cl"> }
</span></span></code></pre></div><h2 id="going-for-zero">Going for zero</h2>
<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>
<p>Gradle&rsquo;s repositories APIs also support the notion of specifying the expected &ldquo;contents&rdquo; 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>
<p>These filters can be of two types:</p>
<ul>
<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>
<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>
</ul>
<p>The difference is subtle, but should become clearer shortly as we start hacking on our setup.</p>
<p>For our plugins block, we want only gMaven to supply AGP and everything else can come from Maven Central. Here&rsquo;s how to achieve that:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"> // settings.gradle.kts
</span></span><span class="line"><span class="cl"> pluginManagement {
</span></span><span class="line"><span class="cl">   repositories {
</span></span><span class="line"><span class="cl"><span class="gd">-    google()
</span></span></span><span class="line"><span class="cl"><span class="gi">+    exclusiveContent { // First type of filter
</span></span></span><span class="line"><span class="cl"><span class="gi">+      forRepository { google() } // Specify the repository this applies to
</span></span></span><span class="line"><span class="cl"><span class="gi">+      filter { // Start specifying what dependencies are *only* found in this repo
</span></span></span><span class="line"><span class="cl"><span class="gi">+        includeGroupAndSubgroups(&#34;androidx&#34;)
</span></span></span><span class="line"><span class="cl"><span class="gi">+        includeGroupAndSubgroups(&#34;com.android&#34;)
</span></span></span><span class="line"><span class="cl"><span class="gi">+        includeGroup(&#34;com.google.testing.platform&#34;)
</span></span></span><span class="line"><span class="cl"><span class="gi">+      }
</span></span></span><span class="line"><span class="cl"><span class="gi">+    }
</span></span></span><span class="line"><span class="cl">     mavenCentral()
</span></span><span class="line"><span class="cl">   }
</span></span><span class="line"><span class="cl"> }
</span></span></code></pre></div><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&rsquo;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&rsquo;t find it there. To allow this fallback, we instead use a <code>content</code> filter as follows.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"> dependencyResolutionManagement {
</span></span><span class="line"><span class="cl">   repositories {
</span></span><span class="line"><span class="cl"><span class="gd">-    google()
</span></span></span><span class="line"><span class="cl"><span class="gi">+    google {
</span></span></span><span class="line"><span class="cl"><span class="gi">+      content {
</span></span></span><span class="line"><span class="cl"><span class="gi">+        includeGroupAndSubgroups(&#34;androidx&#34;)
</span></span></span><span class="line"><span class="cl"><span class="gi">+        includeGroupAndSubgroups(&#34;com.android&#34;)
</span></span></span><span class="line"><span class="cl"><span class="gi">+        includeGroupAndSubgroups(&#34;com.google&#34;)
</span></span></span><span class="line"><span class="cl"><span class="gi">+      }
</span></span></span><span class="line"><span class="cl"><span class="gi">+    }
</span></span></span><span class="line"><span class="cl"><span class="gi">+    maven(&#34;https://androidx.dev/storage/compose-compiler/repository&#34;) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+      name = &#34;Compose Compiler Snapshots&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+      content { includeGroup(&#34;androidx.compose.compiler&#34;) }
</span></span></span><span class="line"><span class="cl"><span class="gi">+    }
</span></span></span><span class="line"><span class="cl">     mavenCentral()
</span></span><span class="line"><span class="cl">     maven(&#34;https://jitpack.io&#34;) { name = &#34;JitPack&#34; }
</span></span><span class="line"><span class="cl">     maven(&#34;https://oss.sonatype.org/content/repositories/snapshots/&#34;) {
</span></span><span class="line"><span class="cl">       name = &#34;Sonatype snapshots
</span></span><span class="line"><span class="cl">     }
</span></span><span class="line"><span class="cl">   }
</span></span><span class="line"><span class="cl"> }
</span></span></code></pre></div><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>
<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&rsquo;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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl">dependencyResolutionManagement {
</span></span><span class="line"><span class="cl">  repositories {
</span></span><span class="line"><span class="cl">    google {
</span></span><span class="line"><span class="cl">      content {
</span></span><span class="line"><span class="cl">        includeGroupAndSubgroups(&#34;androidx&#34;)
</span></span><span class="line"><span class="cl">        includeGroupAndSubgroups(&#34;com.android&#34;)
</span></span><span class="line"><span class="cl">        includeGroupAndSubgroups(&#34;com.google&#34;)
</span></span><span class="line"><span class="cl">      }
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">    maven(&#34;https://androidx.dev/storage/compose-compiler/repository&#34;) {
</span></span><span class="line"><span class="cl">      name = &#34;Compose Compiler Snapshots&#34;
</span></span><span class="line"><span class="cl">      content { includeGroup(&#34;androidx.compose.compiler&#34;) }
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">    mavenCentral()
</span></span><span class="line"><span class="cl"><span class="gd">-    maven(&#34;https://jitpack.io&#34;) { name = &#34;JitPack&#34; }
</span></span></span><span class="line"><span class="cl"><span class="gi">+    exclusiveContent {
</span></span></span><span class="line"><span class="cl"><span class="gi">+      forRepository { maven(&#34;https://jitpack.io&#34;) { name = &#34;JitPack&#34; } }
</span></span></span><span class="line"><span class="cl"><span class="gi">+      filter { includeGroup(&#34;com.github.requery&#34;) }
</span></span></span><span class="line"><span class="cl"><span class="gi">+    }
</span></span></span><span class="line"><span class="cl">    maven(&#34;https://oss.sonatype.org/content/repositories/snapshots/&#34;)
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><p>The Sonatype OSS snapshots repository is only intended to be used for snapshot releases of dependencies we&rsquo;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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl">dependencyResolutionManagement {
</span></span><span class="line"><span class="cl">  repositories {
</span></span><span class="line"><span class="cl">    google {
</span></span><span class="line"><span class="cl">      content {
</span></span><span class="line"><span class="cl">        includeGroupAndSubgroups(&#34;androidx&#34;)
</span></span><span class="line"><span class="cl">        includeGroupAndSubgroups(&#34;com.android&#34;)
</span></span><span class="line"><span class="cl">        includeGroupAndSubgroups(&#34;com.google&#34;)
</span></span><span class="line"><span class="cl">      }
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">    maven(&#34;https://androidx.dev/storage/compose-compiler/repository&#34;) {
</span></span><span class="line"><span class="cl">      name = &#34;Compose Compiler Snapshots&#34;
</span></span><span class="line"><span class="cl">      content { includeGroup(&#34;androidx.compose.compiler&#34;) }
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">    mavenCentral()
</span></span><span class="line"><span class="cl">    maven(&#34;https://jitpack.io&#34;)
</span></span><span class="line"><span class="cl">    exclusiveContent {
</span></span><span class="line"><span class="cl">      forRepository { maven(&#34;https://jitpack.io&#34;) { name = &#34;JitPack&#34; } }
</span></span><span class="line"><span class="cl">      filter { includeGroup(&#34;com.github.requery&#34;) }
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">    maven(&#34;https://oss.sonatype.org/content/repositories/snapshots/&#34;) {
</span></span><span class="line"><span class="cl">      name = &#34;Sonatype Snapshots&#34;
</span></span><span class="line"><span class="cl"><span class="gi">+     mavenContent {
</span></span></span><span class="line"><span class="cl"><span class="gi">+       snapshotsOnly()
</span></span></span><span class="line"><span class="cl"><span class="gi">+     }
</span></span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><h3 id="bonus-section-kotlin-multiplatform">Bonus section: Kotlin Multiplatform</h3>
<p>If you&rsquo;re working with Kotlin Multiplatform, these directions will sadly not cover all the dependencies being fetched during your build. There&rsquo;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&rsquo;s the missing bits:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="n">dependencyResolutionManagement</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">repositories</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// workaround for https://youtrack.jetbrains.com/issue/KT-51379
</span></span></span><span class="line"><span class="cl">    <span class="n">exclusiveContent</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">forRepository</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">ivy</span><span class="p">(</span><span class="s2">&#34;https://download.jetbrains.com/kotlin/native/builds&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="n">name</span> <span class="p">=</span> <span class="s2">&#34;Kotlin Native&#34;</span>
</span></span><span class="line"><span class="cl">          <span class="n">patternLayout</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="n">listOf</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;macos-x86_64&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;macos-aarch64&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;osx-x86_64&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;osx-aarch64&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;linux-x86_64&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;windows-x86_64&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">              <span class="p">)</span>
</span></span><span class="line"><span class="cl">              <span class="p">.</span><span class="n">forEach</span> <span class="p">{</span> <span class="n">os</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">                <span class="n">listOf</span><span class="p">(</span><span class="s2">&#34;dev&#34;</span><span class="p">,</span> <span class="s2">&#34;releases&#34;</span><span class="p">).</span><span class="n">forEach</span> <span class="p">{</span> <span class="n">stage</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">                  <span class="n">artifact</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">$stage</span><span class="s2">/[revision]/</span><span class="si">$os</span><span class="s2">/[artifact]-[revision].[ext]&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="cl">              <span class="p">}</span>
</span></span><span class="line"><span class="cl">          <span class="p">}</span>
</span></span><span class="line"><span class="cl">          <span class="n">metadataSources</span> <span class="p">{</span> <span class="n">artifact</span><span class="p">()</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="n">filter</span> <span class="p">{</span> <span class="n">includeModuleByRegex</span><span class="p">(</span><span class="s2">&#34;.*&#34;</span><span class="p">,</span> <span class="s2">&#34;.*kotlin-native-prebuilt.*&#34;</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">exclusiveContent</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">forRepository</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">ivy</span><span class="p">(</span><span class="s2">&#34;https://nodejs.org/dist/&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="n">name</span> <span class="p">=</span> <span class="s2">&#34;Node Distributions at </span><span class="si">$url</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">          <span class="n">patternLayout</span> <span class="p">{</span> <span class="n">artifact</span><span class="p">(</span><span class="s2">&#34;v[revision]/[artifact](-v[revision]-[classifier]).[ext]&#34;</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">          <span class="n">metadataSources</span> <span class="p">{</span> <span class="n">artifact</span><span class="p">()</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">          <span class="n">content</span> <span class="p">{</span> <span class="n">includeModule</span><span class="p">(</span><span class="s2">&#34;org.nodejs&#34;</span><span class="p">,</span> <span class="s2">&#34;node&#34;</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="n">filter</span> <span class="p">{</span> <span class="n">includeGroup</span><span class="p">(</span><span class="s2">&#34;org.nodejs&#34;</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">exclusiveContent</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">forRepository</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">ivy</span><span class="p">(</span><span class="s2">&#34;https://github.com/yarnpkg/yarn/releases/download&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="n">name</span> <span class="p">=</span> <span class="s2">&#34;Yarn Distributions at </span><span class="si">$url</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">          <span class="n">patternLayout</span> <span class="p">{</span> <span class="n">artifact</span><span class="p">(</span><span class="s2">&#34;v[revision]/[artifact](-v[revision]).[ext]&#34;</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">          <span class="n">metadataSources</span> <span class="p">{</span> <span class="n">artifact</span><span class="p">()</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">          <span class="n">content</span> <span class="p">{</span> <span class="n">includeModule</span><span class="p">(</span><span class="s2">&#34;com.yarnpkg&#34;</span><span class="p">,</span> <span class="s2">&#34;yarn&#34;</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="n">filter</span> <span class="p">{</span> <span class="n">includeGroup</span><span class="p">(</span><span class="s2">&#34;com.yarnpkg&#34;</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>If you&rsquo;re not using a JavaScript target in your project, it should be safe to skip the NodeJS and Yarn repositories but it&rsquo;s probably easier to keep it configured ahead of time in case you adopt JavaScript in the future.</p>
<h2 id="conclusion">Conclusion</h2>
<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>
<h3 id="before">Before</h3>
<figure>
    <img loading="lazy" src="before-fixes.webp"/> <figcaption>
            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>

<h3 id="after">After</h3>
<figure>
    <img loading="lazy" src="after-fixes.webp"/> <figcaption>
            The Android Studio Dependency Sync window, now showing the total sync taking only 3 minutes and 17 seconds with 0 failed requests
        </figcaption>
</figure>

<p>Like and subscribe, and hit that notification bell so you don&rsquo;t miss my next post some time within this decade (hopefully).</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Tips and tricks for using Renovate</title>
      <link>https://msfjarvis.dev/posts/tips-and-tricks-for-using-renovate/</link>
      <pubDate>Wed, 18 Jan 2023 01:02:18 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/tips-and-tricks-for-using-renovate/</guid>
      <description>Renovate is an extremely powerful tool for keeping your dependencies up-to-date, and its flexibility is often left unexplored. I&amp;rsquo;m hoping to change that</description>
      <content:encoded><![CDATA[<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&rsquo;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&rsquo;s a good chance Renovate does it better.</p>
<p>I&rsquo;m collecting some tips here about &ldquo;fancy&rdquo; things I&rsquo;ve done using Renovate that may be helpful to other folks. You&rsquo;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>
<h2 id="disabling-updates-for-individual-packages">Disabling updates for individual packages</h2>
<p>There are times where you&rsquo;re sticking with an older version of a package (temporarily or otherwise) and you just don&rsquo;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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;packageRules&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;managers&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;gradle&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;packagePatterns&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;^com.squareup.okhttp3&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;enabled&#34;</span><span class="p">:</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="grouping-updates-together">Grouping updates together</h2>
<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&rsquo;s how you can combine updates of the serde crate and its derive macro.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;packageRules&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;managers&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;cargo&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;matchPackagePatterns&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;serde&#34;</span><span class="p">,</span> <span class="s2">&#34;serde_derive&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;groupName&#34;</span><span class="p">:</span> <span class="s2">&#34;serde&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="set-a-semver-range-for-upgrades">Set a semver range for upgrades</h2>
<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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;packageRules&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;matchPackageNames&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;com.android.tools.build:gradle&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;allowedVersions&#34;</span><span class="p">:</span> <span class="s2">&#34;&lt;=7.4.0&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="supporting-non-standard-dependency-declarations">Supporting non-standard dependency declarations</h2>
<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>
<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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">build</span><span class="p">.</span><span class="nx">environment</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">HUGO_VERSION</span> <span class="p">=</span> <span class="s2">&#34;0.109.0&#34;</span>
</span></span></code></pre></div><p>This is how the relevant configuration might look like with Renovate</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;regexManagers&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;description&#34;</span><span class="p">:</span> <span class="s2">&#34;Update Hugo version in Netlify config&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;fileMatch&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;.toml$&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;matchStrings&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;HUGO_VERSION = \&#34;(?&lt;currentValue&gt;.*?)\&#34;&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;depNameTemplate&#34;</span><span class="p">:</span> <span class="s2">&#34;gohugoio/hugo&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;datasourceTemplate&#34;</span><span class="p">:</span> <span class="s2">&#34;github-releases&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>You can read more about Regex Managers <a href="https://docs.renovatebot.com/modules/manager/regex/">here</a>.</p>
<h2 id="making-your-github-actions-usage-more-secure">Making your GitHub Actions usage more secure</h2>
<p>According to GitHub&rsquo;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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;extends&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;config:base&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;:dependencyDashboard&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;helpers:pinGitHubActionDigests&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="automatically-merging-compatible-updates">Automatically merging compatible updates</h2>
<p>Every person with a JavaScript project has definitely loved getting 20 PRs from Dependabot about arbitrary transitive dependencies that they didn&rsquo;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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;automergeType&#34;</span><span class="p">:</span> <span class="s2">&#34;branch&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;packageRules&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;description&#34;</span><span class="p">:</span> <span class="s2">&#34;Automerge non-major updates&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;matchUpdateTypes&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;minor&#34;</span><span class="p">,</span> <span class="s2">&#34;patch&#34;</span><span class="p">,</span> <span class="s2">&#34;digest&#34;</span><span class="p">,</span> <span class="s2">&#34;lockFileMaintenance&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;automerge&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"> name: Run tests
</span></span><span class="line"><span class="cl"> on:
</span></span><span class="line"><span class="cl">   pull_request:
</span></span><span class="line"><span class="cl">     branches:
</span></span><span class="line"><span class="cl">       - main
</span></span><span class="line"><span class="cl"><span class="gi">+  push:
</span></span></span><span class="line"><span class="cl"><span class="gi">+    branches:
</span></span></span><span class="line"><span class="cl"><span class="gi">+      - renovate/**
</span></span></span></code></pre></div><h2 id="closing-notes">Closing notes</h2>
<p>This list currently consists exclusively of things I&rsquo;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://androiddev.social/@msfjarvis">@msfjarvis@androiddev.social</a>!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Writing your own Nix Flake checks</title>
      <link>https://msfjarvis.dev/posts/writing-your-own-nix-flake-checks/</link>
      <pubDate>Sun, 18 Dec 2022 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/writing-your-own-nix-flake-checks/</guid>
      <description>Quick how-to for writing ad-hoc checks for your own Nix Flakes</description>
      <content:encoded><![CDATA[<h2 id="preface">Preface</h2>
<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&rsquo;re building Rust things), I&rsquo;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&rsquo;ll share below.</p>
<h2 id="the-premise">The premise</h2>
<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&rsquo;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>
<h2 id="the-thing-youre-here-for">The thing you&rsquo;re here for</h2>
<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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="n">outputs</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">self</span><span class="o">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">nixpkgs</span><span class="o">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">flake-utils</span><span class="o">,</span>
</span></span><span class="line"><span class="cl"><span class="p">}:</span>
</span></span><span class="line"><span class="cl">  <span class="n">flake-utils</span><span class="o">.</span><span class="n">lib</span><span class="o">.</span><span class="n">eachDefaultSystem</span> <span class="p">(</span><span class="n">system</span><span class="p">:</span> <span class="k">let</span>
</span></span><span class="line"><span class="cl">    <span class="n">pkgs</span> <span class="o">=</span> <span class="kn">import</span> <span class="n">nixpkgs</span> <span class="p">{</span><span class="k">inherit</span> <span class="n">system</span><span class="p">;};</span>
</span></span><span class="line"><span class="cl">    <span class="n">files</span> <span class="o">=</span> <span class="n">pkgs</span><span class="o">.</span><span class="n">lib</span><span class="o">.</span><span class="n">concatStringsSep</span> <span class="s2">&#34; &#34;</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># Individual shell scripts from the repository</span>
</span></span><span class="line"><span class="cl">    <span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="n">fmt-check</span> <span class="o">=</span> <span class="n">pkgs</span><span class="o">.</span><span class="n">stdenv</span><span class="o">.</span><span class="n">mkDerivation</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">name</span> <span class="o">=</span> <span class="s2">&#34;fmt-check&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">src</span> <span class="o">=</span> <span class="sr">./.</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">doCheck</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">nativeBuildInputs</span> <span class="o">=</span> <span class="k">with</span> <span class="n">pkgs</span><span class="p">;</span> <span class="p">[</span><span class="n">alejandra</span> <span class="n">shellcheck</span> <span class="n">shfmt</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">      <span class="n">checkPhase</span> <span class="o">=</span> <span class="s1">&#39;&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">        shfmt -d -s -i 2 -ci </span><span class="si">${</span><span class="n">files</span><span class="si">}</span><span class="s1">
</span></span></span><span class="line"><span class="cl"><span class="s1">        alejandra -c .
</span></span></span><span class="line"><span class="cl"><span class="s1">        shellcheck -x </span><span class="si">${</span><span class="n">files</span><span class="si">}</span><span class="s1">
</span></span></span><span class="line"><span class="cl"><span class="s1">      &#39;&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">  <span class="k">in</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">checks</span> <span class="o">=</span> <span class="p">{</span><span class="k">inherit</span> <span class="n">fmt-check</span><span class="p">;};</span>
</span></span><span class="line"><span class="cl">  <span class="p">});</span>
</span></span></code></pre></div><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&rsquo;s the <code>files</code> binding declared in the snippet above.</p>
<p>Here I ran into my first problem: Nix expects every derivation to generate an output which meant this doesn&rsquo;t actually build.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">➜ nix flake check
</span></span><span class="line"><span class="cl">error: flake attribute &#39;checks.fmt-check.outPath&#39; is not a derivation
</span></span></code></pre></div><p>There&rsquo;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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gh">diff --git flake.nix flake.nix
</span></span></span><span class="line"><span class="cl"><span class="gh">index b7fef3b99110..a531a30ad88e 100644
</span></span></span><span class="line"><span class="cl"><span class="gd">--- flake.nix
</span></span></span><span class="line"><span class="cl"><span class="gi">+++ flake.nix
</span></span></span><span class="line"><span class="cl"><span class="gu">@@ -18,6 +18,7 @@
</span></span></span><span class="line"><span class="cl">       ];
</span></span><span class="line"><span class="cl">       fmt-check = pkgs.stdenv.mkDerivation {
</span></span><span class="line"><span class="cl">         name = &#34;fmt-check&#34;;
</span></span><span class="line"><span class="cl"><span class="gi">+        dontBuild = true;
</span></span></span><span class="line"><span class="cl">         src = ./.;
</span></span><span class="line"><span class="cl">         doCheck = true;
</span></span><span class="line"><span class="cl">         nativeBuildInputs = with pkgs; [alejandra shellcheck shfmt];
</span></span><span class="line"><span class="cl">         checkPhase = &#39;&#39;
</span></span><span class="line"><span class="cl"><span class="gu">@@ -25,6 +26,11 @@
</span></span></span><span class="line"><span class="cl">           alejandra -c .
</span></span><span class="line"><span class="cl">           shellcheck -x ${files}
</span></span><span class="line"><span class="cl">         &#39;&#39;;
</span></span><span class="line"><span class="cl"><span class="gi">+        installPhase = &#39;&#39;
</span></span></span><span class="line"><span class="cl"><span class="gi">+          mkdir &#34;$out&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+        &#39;&#39;;
</span></span></span><span class="line"><span class="cl">       };
</span></span><span class="line"><span class="cl">     in {
</span></span><span class="line"><span class="cl">       checks = {inherit fmt-check;};
</span></span></code></pre></div><p><code>dontBuild</code> does exactly what you&rsquo;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>
<p>You can make this slightly faster by using a smaller stdenv that won&rsquo;t pull in a compiler toolchain or be rebuilt when said toolchain is updated:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gh">diff --git flake.nix flake.nix
</span></span></span><span class="line"><span class="cl"><span class="gh">index 7ce7a2ba80f8..b69db13fbc6d 100644
</span></span></span><span class="line"><span class="cl"><span class="gd">--- flake.nix
</span></span></span><span class="line"><span class="cl"><span class="gi">+++ flake.nix
</span></span></span><span class="line"><span class="cl"><span class="gu">@@ -16,7 +16,7 @@
</span></span></span><span class="line"><span class="cl">       files = pkgs.lib.concatStringsSep &#34; &#34; [
</span></span><span class="line"><span class="cl">         # bunch of shell scripts since I didn&#39;t have an extension I could glob against
</span></span><span class="line"><span class="cl">       ];
</span></span><span class="line"><span class="cl"><span class="gd">-      fmt-check = pkgs.stdenv.mkDerivation {
</span></span></span><span class="line"><span class="cl"><span class="gi">+      fmt-check = pkgs.stdenvNoCC.mkDerivation {
</span></span></span><span class="line"><span class="cl">         name = &#34;fmt-check&#34;;
</span></span><span class="line"><span class="cl">         dontBuild = true;
</span></span><span class="line"><span class="cl">         src = ./.;
</span></span></code></pre></div><h2 id="the-final-result">The final result</h2>
<p>This is what the flake looked like for me after all this</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="o">=</span> <span class="s2">&#34;A very basic flake&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">inputs</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">nixpkgs</span><span class="o">.</span><span class="n">url</span> <span class="o">=</span> <span class="s2">&#34;github:nixos/nixpkgs/nixpkgs-unstable&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">flake-utils</span><span class="o">.</span><span class="n">url</span> <span class="o">=</span> <span class="s2">&#34;github:numtide/flake-utils/main&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">outputs</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">self</span><span class="o">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">nixpkgs</span><span class="o">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">flake-utils</span><span class="o">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">}:</span>
</span></span><span class="line"><span class="cl">    <span class="n">flake-utils</span><span class="o">.</span><span class="n">lib</span><span class="o">.</span><span class="n">eachDefaultSystem</span> <span class="p">(</span><span class="n">system</span><span class="p">:</span> <span class="k">let</span>
</span></span><span class="line"><span class="cl">      <span class="n">pkgs</span> <span class="o">=</span> <span class="kn">import</span> <span class="n">nixpkgs</span> <span class="p">{</span><span class="k">inherit</span> <span class="n">system</span><span class="p">;};</span>
</span></span><span class="line"><span class="cl">      <span class="n">files</span> <span class="o">=</span> <span class="n">pkgs</span><span class="o">.</span><span class="n">lib</span><span class="o">.</span><span class="n">concatStringsSep</span> <span class="s2">&#34; &#34;</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Individual shell scripts from the repository</span>
</span></span><span class="line"><span class="cl">      <span class="p">];</span>
</span></span><span class="line"><span class="cl">      <span class="n">fmt-check</span> <span class="o">=</span> <span class="n">pkgs</span><span class="o">.</span><span class="n">stdenvNoCC</span><span class="o">.</span><span class="n">mkDerivation</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">name</span> <span class="o">=</span> <span class="s2">&#34;fmt-check&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">dontBuild</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">src</span> <span class="o">=</span> <span class="sr">./.</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">doCheck</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">nativeBuildInputs</span> <span class="o">=</span> <span class="k">with</span> <span class="n">pkgs</span><span class="p">;</span> <span class="p">[</span><span class="n">alejandra</span> <span class="n">shellcheck</span> <span class="n">shfmt</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">        <span class="n">checkPhase</span> <span class="o">=</span> <span class="s1">&#39;&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">          shfmt -d -s -i 2 -ci </span><span class="si">${</span><span class="n">files</span><span class="si">}</span><span class="s1">
</span></span></span><span class="line"><span class="cl"><span class="s1">          alejandra -c .
</span></span></span><span class="line"><span class="cl"><span class="s1">          shellcheck -x </span><span class="si">${</span><span class="n">files</span><span class="si">}</span><span class="s1">
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#39;&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">installPhase</span> <span class="o">=</span> <span class="s1">&#39;&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">          mkdir &#34;$out&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#39;&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="k">in</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">checks</span> <span class="o">=</span> <span class="p">{</span><span class="k">inherit</span> <span class="n">fmt-check</span><span class="p">;};</span>
</span></span><span class="line"><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>It&rsquo;s probably not idiomatic Nix (for some definition of idiomatic) but the entire thing has been a trial and error anyway so &#x1f937;</p>
<p>I&rsquo;m very much a noob when it comes to Nix so any feedback is very welcome and appreciated!</p>
<h3 id="alternative-solution-june-2024-update">Alternative solution (June 2024 update)</h3>
<p>The above &lsquo;hack&rsquo; 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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="o">=</span> <span class="s2">&#34;A very basic flake&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">inputs</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">nixpkgs</span><span class="o">.</span><span class="n">url</span> <span class="o">=</span> <span class="s2">&#34;github:nixos/nixpkgs/nixpkgs-unstable&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">flake-utils</span><span class="o">.</span><span class="n">url</span> <span class="o">=</span> <span class="s2">&#34;github:numtide/flake-utils/main&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">outputs</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">self</span><span class="o">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">nixpkgs</span><span class="o">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">flake-utils</span><span class="o">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">}:</span>
</span></span><span class="line"><span class="cl">    <span class="n">flake-utils</span><span class="o">.</span><span class="n">lib</span><span class="o">.</span><span class="n">eachDefaultSystem</span> <span class="p">(</span><span class="n">system</span><span class="p">:</span> <span class="k">let</span>
</span></span><span class="line"><span class="cl">      <span class="n">pkgs</span> <span class="o">=</span> <span class="kn">import</span> <span class="n">nixpkgs</span> <span class="p">{</span><span class="k">inherit</span> <span class="n">system</span><span class="p">;};</span>
</span></span><span class="line"><span class="cl">      <span class="n">files</span> <span class="o">=</span> <span class="n">pkgs</span><span class="o">.</span><span class="n">lib</span><span class="o">.</span><span class="n">concatStringsSep</span> <span class="s2">&#34; &#34;</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Individual shell scripts from the repository</span>
</span></span><span class="line"><span class="cl">      <span class="p">];</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># Variant of runCommand intended for commands</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># that run quickly and will be slowed down by</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># the network round-trip.</span>
</span></span><span class="line"><span class="cl">      <span class="n">fmt-check</span> <span class="o">=</span> <span class="n">pkgs</span><span class="o">.</span><span class="n">runCommandLocal</span> <span class="s2">&#34;fmt-check&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">src</span> <span class="o">=</span> <span class="sr">./.</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">nativeBuildInputs</span> <span class="o">=</span> <span class="k">with</span> <span class="n">pkgs</span><span class="p">;</span> <span class="p">[</span><span class="n">alejandra</span> <span class="n">shellcheck</span> <span class="n">shfmt</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span> <span class="s1">&#39;&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">          shfmt -d -s -i 2 -ci </span><span class="si">${</span><span class="n">files</span><span class="si">}</span><span class="s1">
</span></span></span><span class="line"><span class="cl"><span class="s1">          alejandra -c .
</span></span></span><span class="line"><span class="cl"><span class="s1">          shellcheck -x </span><span class="si">${</span><span class="n">files</span><span class="si">}</span><span class="s1">
</span></span></span><span class="line"><span class="cl"><span class="s1">          mkdir &#34;$out&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#39;&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">in</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">checks</span> <span class="o">=</span> <span class="p">{</span><span class="k">inherit</span> <span class="n">fmt-check</span><span class="p">;};</span>
</span></span><span class="line"><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div>]]></content:encoded>
    </item>
    
    <item>
      <title>Mastodon on your own domain without hosting a server, Netlify edition</title>
      <link>https://msfjarvis.dev/posts/mastodon-on-your-own-domain-without-hosting-a-server-netlify-edition/</link>
      <pubDate>Wed, 16 Nov 2022 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/mastodon-on-your-own-domain-without-hosting-a-server-netlify-edition/</guid>
      <description>A quick and easy way of creating a Fediverse identity on your own domain without an ActivityPub server</description>
      <content:encoded><![CDATA[<h2 id="preface">Preface</h2>
<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>
<p>I recommend going to their blog and reading the whole thing, but here&rsquo;s a TL;DR</p>
<ul>
<li><a href="https://activitypub.rocks/">ActivityPub</a> has the notion of an &ldquo;actor&rdquo; that sends messages</li>
<li>This &ldquo;actor&rdquo; must be discoverable via a protocol called <a href="https://webfinger.net">WebFinger</a></li>
<li>WebFinger is ridiculously easy to implement</li>
</ul>
<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 &ldquo;actors&rdquo; across the Fediverse.</p>
<p>Maarten&rsquo;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>
<p>Maarten did however note that since they&rsquo;re relying on static hosting, they&rsquo;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>
<h2 id="the-implementation">The implementation</h2>
<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>
<p>Similar to Maarten, I first obtained my current Fediverse identity from the Mastodon server I am on: <a href="https://androiddev.social">androiddev.social</a> (incredible props to <a href="https://androiddev.social/@friendlymike">Mikhail</a> for making it a reality).</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="err">➜</span> <span class="err">curl</span> <span class="err">-s</span> <span class="err">https:</span><span class="c1">//androiddev.social/.well-known/webfinger?resource=acct:msfjarvis@androiddev.social | jq .
</span></span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;subject&#34;</span><span class="p">:</span> <span class="s2">&#34;acct:msfjarvis@androiddev.social&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;aliases&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;https://androiddev.social/@msfjarvis&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;https://androiddev.social/users/msfjarvis&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;links&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;rel&#34;</span><span class="p">:</span> <span class="s2">&#34;http://webfinger.net/rel/profile-page&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;text/html&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;href&#34;</span><span class="p">:</span> <span class="s2">&#34;https://androiddev.social/@msfjarvis&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;rel&#34;</span><span class="p">:</span> <span class="s2">&#34;self&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;application/activity+json&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;href&#34;</span><span class="p">:</span> <span class="s2">&#34;https://androiddev.social/users/msfjarvis&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;rel&#34;</span><span class="p">:</span> <span class="s2">&#34;http://ostatus.org/schema/1.0/subscribe&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;template&#34;</span><span class="p">:</span> <span class="s2">&#34;https://androiddev.social/authorize_interaction?uri={uri}&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>With this in hand, now we can get started on wiring this up into our website.</p>
<p>First, create an Edge Function using the Netlify CLI. Here&rsquo;s the options I chose.</p>
<pre tabindex="0"><code>➜ yarn exec ntl functions:create --name webfinger
? Select the type of function you&#39;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 &#39;webfinger&#39; registered for route `/.well-known/webfinger`. To change, edit your `netlify.toml` file.
</code></pre><p>Next, add the following code to the TypeScript file just created for you. I&rsquo;ve added comments inline to explain what each part of the code does so you can customize it according to your needs.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="c1">// Netlify Edge Functions run on Deno (https://deno.land), so imports use URLs rather than package names.
</span></span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">Status</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;https://deno.land/std@0.136.0/http/http_status.ts&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="kr">type</span> <span class="p">{</span> <span class="nx">Context</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;https://edge.netlify.com&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="kr">async</span> <span class="p">(</span><span class="nx">request</span>: <span class="kt">Request</span><span class="p">,</span> <span class="nx">context</span>: <span class="kt">Context</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// We obtain the value of the &#39;resource&#39; query parameter so that we
</span></span></span><span class="line"><span class="cl">  <span class="c1">// can ensure a response is only sent for the identity we want.
</span></span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URL</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">resourceParam</span> <span class="o">=</span> <span class="nx">url</span><span class="p">.</span><span class="nx">searchParams</span><span class="p">.</span><span class="kr">get</span><span class="p">(</span><span class="s2">&#34;resource&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">resourceParam</span> <span class="o">===</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">context</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">error</span><span class="o">:</span> <span class="s2">&#34;No &#39;resource&#39; query parameter was provided&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">status</span>: <span class="kt">Status.BadRequest</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// I want to be searchable as `@harsh@msfjarvis.dev`, so I only
</span></span></span><span class="line"><span class="cl">    <span class="c1">// allow requests that set the resource query param to this value.
</span></span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">resourceParam</span> <span class="o">!==</span> <span class="s2">&#34;acct:harsh@msfjarvis.dev&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">context</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">error</span><span class="o">:</span> <span class="s2">&#34;An invalid identity was requested&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">status</span>: <span class="kt">Status.BadRequest</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Here&#39;s the JSON object we got earlier
</span></span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">context</span><span class="p">.</span><span class="nx">json</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">      <span class="nx">subject</span><span class="o">:</span> <span class="s2">&#34;acct:msfjarvis@androiddev.social&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">aliases</span><span class="o">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;https://androiddev.social/@msfjarvis&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;https://androiddev.social/users/msfjarvis&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="p">],</span>
</span></span><span class="line"><span class="cl">      <span class="nx">links</span><span class="o">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nx">rel</span><span class="o">:</span> <span class="s2">&#34;http://webfinger.net/rel/profile-page&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="kr">type</span><span class="o">:</span> <span class="s2">&#34;text/html&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nx">href</span><span class="o">:</span> <span class="s2">&#34;https://androiddev.social/@msfjarvis&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nx">rel</span><span class="o">:</span> <span class="s2">&#34;self&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="kr">type</span><span class="o">:</span> <span class="s2">&#34;application/activity+json&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nx">href</span><span class="o">:</span> <span class="s2">&#34;https://androiddev.social/users/msfjarvis&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nx">rel</span><span class="o">:</span> <span class="s2">&#34;http://ostatus.org/schema/1.0/subscribe&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nx">template</span><span class="o">:</span> <span class="s2">&#34;https://androiddev.social/authorize_interaction?uri={uri}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span></code></pre></div><p>And that&rsquo;s it! You can test it out as below to verify things work as expected.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="err">➜</span> <span class="err">curl</span> <span class="err">-s</span> <span class="err">https:</span><span class="c1">//msfjarvis.dev/.well-known/webfinger | jq .
</span></span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;error&#34;</span><span class="p">:</span> <span class="s2">&#34;No &#39;resource&#39; query parameter was provided&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">➜</span> <span class="err">curl</span> <span class="err">-s</span> <span class="err">https:</span><span class="c1">//msfjarvis.dev/.well-known/webfinger?resource=acct:anything@msfjarvis.dev | jq .
</span></span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;error&#34;</span><span class="p">:</span> <span class="s2">&#34;An invalid identity was requested&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">➜</span> <span class="err">curl</span> <span class="err">-s</span> <span class="err">https:</span><span class="c1">//msfjarvis.dev/.well-known/webfinger?resource=acct:harsh@msfjarvis.dev | jq .
</span></span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;subject&#34;</span><span class="p">:</span> <span class="s2">&#34;acct:msfjarvis@androiddev.social&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;aliases&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;https://androiddev.social/@msfjarvis&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;https://androiddev.social/users/msfjarvis&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;links&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;rel&#34;</span><span class="p">:</span> <span class="s2">&#34;http://webfinger.net/rel/profile-page&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;text/html&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;href&#34;</span><span class="p">:</span> <span class="s2">&#34;https://androiddev.social/@msfjarvis&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;rel&#34;</span><span class="p">:</span> <span class="s2">&#34;self&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;application/activity+json&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;href&#34;</span><span class="p">:</span> <span class="s2">&#34;https://androiddev.social/users/msfjarvis&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;rel&#34;</span><span class="p">:</span> <span class="s2">&#34;http://ostatus.org/schema/1.0/subscribe&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;template&#34;</span><span class="p">:</span> <span class="s2">&#34;https://androiddev.social/authorize_interaction?uri={uri}&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Thanks again to Maarten for doing the initial research for this and writing about it!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Writing Paparazzi tests for your Kotlin Multiplatform projects</title>
      <link>https://msfjarvis.dev/posts/writing-paparazzi-tests-for-your-kotlin-multiplatform-projects/</link>
      <pubDate>Sun, 26 Jun 2022 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/writing-paparazzi-tests-for-your-kotlin-multiplatform-projects/</guid>
      <description>Paparazzi enables a radically faster and improved UI testing workflow, and using a small workaround we can bring that to our multiplatform Compose projects</description>
      <content:encoded><![CDATA[<h2 id="introduction">Introduction</h2>
<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>
<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&rsquo; redistribution of Jetpack Compose optimised for Kotlin Multiplatform.</p>
<p>I&rsquo;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>
<h2 id="setting-things-up">Setting things up</h2>
<p>To begin, we&rsquo;ll need a new Gradle module for our Paparazzi tests. Since Paparazzi doesn&rsquo;t understand Kotlin Multiplatform yet, we&rsquo;re gonna hide that aspect of our project and present it a pure Android library project. Set up the module like so:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="c1">// paparazzi-tests/build.gradle.kts
</span></span></span><span class="line"><span class="cl"><span class="n">plugins</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">id</span><span class="p">(</span><span class="s2">&#34;com.android.library&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">id</span><span class="p">(</span><span class="s2">&#34;app.cash.paparazzi&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">android</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">buildFeatures</span> <span class="p">{</span> <span class="n">compose</span> <span class="p">=</span> <span class="k">true</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Now, add dependencies in this module to the modules that contain the composables you&rsquo;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&rsquo;re trying to test the UI exposed by a &ldquo;common&rdquo; module like I am, that might not be such a big deal.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="c1">// paparazzi-tests/build.gradle.kts
</span></span></span><span class="line"><span class="cl"><span class="n">dependencies</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">testImplementation</span><span class="p">(</span><span class="n">projects</span><span class="p">.</span><span class="n">common</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>And that&rsquo;s pretty much it! You can now be off to the races and start writing your tests:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="c1">// paparazzi-tests/src/test/kotlin/UserProfileTest.kt
</span></span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">UserProfileTest</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nd">@get</span><span class="p">:</span><span class="n">Rule</span> <span class="k">val</span> <span class="py">paparazzi</span> <span class="p">=</span> <span class="n">Paparazzi</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nd">@Test</span>
</span></span><span class="line"><span class="cl">  <span class="k">fun</span> <span class="nf">light</span><span class="n">_mode</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">paparazzi</span><span class="p">.</span><span class="n">snapshot</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">MaterialTheme</span><span class="p">(</span><span class="n">colorScheme</span> <span class="p">=</span> <span class="n">LightThemeColors</span><span class="p">)</span> <span class="p">{</span> <span class="n">UserProfile</span><span class="p">()</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nd">@Test</span>
</span></span><span class="line"><span class="cl">  <span class="k">fun</span> <span class="nf">dark</span><span class="n">_mode</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">paparazzi</span><span class="p">.</span><span class="n">snapshot</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">MaterialTheme</span><span class="p">(</span><span class="n">colorScheme</span> <span class="p">=</span> <span class="n">DarkThemeColors</span><span class="p">)</span> <span class="p">{</span> <span class="n">UserProfile</span><span class="p">()</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Consult the <a href="https://cashapp.github.io/paparazzi">Paparazzi documentation</a> for the Gradle tasks reference and customization options.</p>
<h2 id="recipes">Recipes</h2>
<h3 id="disable-release-build-type-for-test-module">Disable release build type for test module</h3>
<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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="c1">// paparazzi-tests/build.gradle.kts
</span></span></span><span class="line"><span class="cl"><span class="n">androidComponents</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">beforeVariants</span> <span class="p">{</span> <span class="n">variant</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="n">variant</span><span class="p">.</span><span class="n">enable</span> <span class="p">=</span> <span class="n">variant</span><span class="p">.</span><span class="n">buildType</span> <span class="o">==</span> <span class="s2">&#34;debug&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="running-with-jdk-12">Running with JDK 12+</h3>
<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&rsquo;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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="c1">// paparazzi-tests/build.gradle.kts
</span></span></span><span class="line"><span class="cl"><span class="n">tasks</span><span class="p">.</span><span class="n">withType</span><span class="p">&lt;</span><span class="n">Test</span><span class="p">&gt;().</span><span class="n">configureEach</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">javaLauncher</span><span class="p">.</span><span class="k">set</span><span class="p">(</span><span class="n">javaToolchains</span><span class="p">.</span><span class="n">launcherFor</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">languageVersion</span><span class="p">.</span><span class="k">set</span><span class="p">(</span><span class="nc">JavaLanguageVersion</span><span class="p">.</span><span class="n">of</span><span class="p">(</span><span class="mi">11</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="testing-with-multiple-themes-easily">Testing with multiple themes easily</h3>
<p>Using an enum and Google&rsquo;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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="c1">// paparazzi-tests/src/test/kotlin/Theme.kt
</span></span></span><span class="line"><span class="cl"><span class="k">import</span> <span class="nn">androidx.compose.material3.ColorScheme</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">enum</span> <span class="k">class</span> <span class="nc">Theme</span><span class="p">(</span><span class="k">val</span> <span class="py">colors</span><span class="p">:</span> <span class="n">ColorScheme</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">Light</span><span class="p">(</span><span class="n">LightThemeColors</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">Dark</span><span class="p">(</span><span class="n">DarkThemeColors</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="c1">// paparazzi-tests/src/test/kotlin/UserProfileTest.kt
</span></span></span><span class="line"><span class="cl"><span class="nd">@RunWith</span><span class="p">(</span><span class="n">TestParameterInjector</span><span class="o">::</span><span class="k">class</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">UserProfileTest</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nd">@get</span><span class="p">:</span><span class="n">Rule</span> <span class="k">val</span> <span class="py">paparazzi</span> <span class="p">=</span> <span class="n">Paparazzi</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nd">@Test</span>
</span></span><span class="line"><span class="cl">  <span class="k">fun</span> <span class="nf">verify</span><span class="p">(</span><span class="nd">@TestParameter</span> <span class="n">theme</span><span class="p">:</span> <span class="n">Theme</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">paparazzi</span><span class="p">.</span><span class="n">snapshot</span><span class="p">(</span><span class="n">name</span> <span class="p">=</span> <span class="n">theme</span><span class="p">.</span><span class="n">name</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">MaterialTheme</span><span class="p">(</span><span class="n">colorScheme</span> <span class="p">=</span> <span class="n">theme</span><span class="p">.</span><span class="n">colors</span><span class="p">)</span> <span class="p">{</span> <span class="n">UserProfile</span><span class="p">()</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div>]]></content:encoded>
    </item>
    
    <item>
      <title>Converting Gradle convention plugins to binary plugins</title>
      <link>https://msfjarvis.dev/posts/converting-gradle-convention-plugins-to-binary-plugins/</link>
      <pubDate>Sun, 17 Apr 2022 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/converting-gradle-convention-plugins-to-binary-plugins/</guid>
      <description>Gradle&amp;rsquo;s convention plugins are a fantastic way to share common build configuration, why not take them a step further?</description>
      <content:encoded><![CDATA[<h3 id="introduction">Introduction</h3>
<p>Gradle&rsquo;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>
<blockquote>
<p><code>com.example.android-library.gradle.kts</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-groovy" data-lang="groovy"><span class="line"><span class="cl"><span class="n">plugins</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">id</span><span class="o">(</span><span class="s2">&#34;com.android.library&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">android</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">buildFeatures</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">buildConfig</span> <span class="o">=</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl">  <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div></blockquote>
<p>Then in your modules, you can use this plugin like so:</p>
<blockquote>
<p><code>library-module/build.gradle.kts</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-groovy" data-lang="groovy"><span class="line"><span class="cl"><span class="n">plugins</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">id</span> <span class="o">(</span><span class="s2">&#34;com.example.android-library&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div></blockquote>
<h2 id="setting-up-convention-plugins-in-your-project">Setting up convention plugins in your project</h2>
<p>Gradle&rsquo;s official sample linked above mentions <code>buildSrc</code> as the location for your convention plugins. I&rsquo;m inclined to disagree, <code>buildSrc</code> has historically had issues with IDE support and it&rsquo;s special status within Gradle&rsquo;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>
<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>
<p>A full explainer on the topic is slightly out of scope for this post, but I can wholeheartedly endorse Jendrik Johannes&rsquo; <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 &lsquo;Understanding Gradle&rsquo; <a href="https://github.com/jjohannes/understanding-gradle#readme">video series</a>.</p>
<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>
<p>First, let&rsquo;s answer this: what is a binary plugin?</p>
<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&rsquo;t exist in the first place.</p>
<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>
<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>
<h2 id="publishing-your-convention-plugins">Publishing your convention plugins</h2>
<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>&rsquo;s convention plugins for Android are:</p>
<blockquote>
<p><code>build-logic/android-plugins/build.gradle.kts</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">-plugins { `kotlin-dsl` }
</span></span></span><span class="line"><span class="cl"><span class="gi">+plugins {
</span></span></span><span class="line"><span class="cl"><span class="gi">+  `kotlin-dsl`
</span></span></span><span class="line"><span class="cl"><span class="gi">+  id(&#34;maven-publish&#34;)
</span></span></span><span class="line"><span class="cl"><span class="gi">+}
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+group = &#34;com.github.android-password-store&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+version = &#34;1.0.0&#34;
</span></span></span></code></pre></div></blockquote>
<p>After that you can run <code>gradle -p build-logic publishToMavenLocal</code> and it will Just Work&#x2122;&#xfe0f;. You can configure additional publishing repositories in similar fashion to how you&rsquo;d do it for a library project.</p>
<p>If like me you need to publish these to <a href="https://search.maven.org/">Maven Central</a>, you&rsquo;ll need slightly more setup since it enforces multiple security and publishing related best practices. Here&rsquo;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>
<blockquote>
<p><code>build-logic/settings.gradle.kts</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gi">+pluginManagement {
</span></span></span><span class="line"><span class="cl"><span class="gi">+  repositories {
</span></span></span><span class="line"><span class="cl"><span class="gi">+    mavenCentral()
</span></span></span><span class="line"><span class="cl"><span class="gi">+    gradlePluginPortal()
</span></span></span><span class="line"><span class="cl"><span class="gi">+  }
</span></span></span><span class="line"><span class="cl"><span class="gi">+  plugins {
</span></span></span><span class="line"><span class="cl"><span class="gi">+    id(&#34;com.vanniktech.maven.publish.base&#34;) version &#34;0.19.0&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+  }
</span></span></span><span class="line"><span class="cl"><span class="gi">+}
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span></code></pre></div></blockquote>
<blockquote>
<p><code>build-logic/android-plugins/build.gradle.kts</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gi">+import com.vanniktech.maven.publish.JavadocJar
</span></span></span><span class="line"><span class="cl"><span class="gi">+import com.vanniktech.maven.publish.JavaLibrary
</span></span></span><span class="line"><span class="cl"><span class="gi">+import com.vanniktech.maven.publish.MavenPublishBaseExtension
</span></span></span><span class="line"><span class="cl"><span class="gi">+import com.vanniktech.maven.publish.SonatypeHost
</span></span></span><span class="line"><span class="cl"><span class="gi">+import org.gradle.kotlin.dsl.provideDelegate
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gd">-plugins { `kotlin-dsl` }
</span></span></span><span class="line"><span class="cl"><span class="gi">+plugins {
</span></span></span><span class="line"><span class="cl"><span class="gi">+  `kotlin-dsl`
</span></span></span><span class="line"><span class="cl"><span class="gi">+  id(&#34;com.vanniktech.maven.publish.base&#34;)
</span></span></span><span class="line"><span class="cl"><span class="gi">+  id(&#34;signing&#34;)
</span></span></span><span class="line"><span class="cl"><span class="gi">+}
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+configure&lt;MavenPublishBaseExtension&gt; {
</span></span></span><span class="line"><span class="cl"><span class="gi">+  group = requireNotNull(project.findProperty(&#34;GROUP&#34;))
</span></span></span><span class="line"><span class="cl"><span class="gi">+  version = requireNotNull(project.findProperty(&#34;VERSION_NAME&#34;))
</span></span></span><span class="line"><span class="cl"><span class="gi">+  publishToMavenCentral(SonatypeHost.DEFAULT)
</span></span></span><span class="line"><span class="cl"><span class="gi">+  signAllPublications()
</span></span></span><span class="line"><span class="cl"><span class="gi">+  configure(JavaLibrary(JavadocJar.Empty()))
</span></span></span><span class="line"><span class="cl"><span class="gi">+  pomFromGradleProperties()
</span></span></span><span class="line"><span class="cl"><span class="gi">+}
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+ afterEvaluate {
</span></span></span><span class="line"><span class="cl"><span class="gi">+  signing {
</span></span></span><span class="line"><span class="cl"><span class="gi">+    val signingKey: String? by project
</span></span></span><span class="line"><span class="cl"><span class="gi">+    val signingPassword: String? by project
</span></span></span><span class="line"><span class="cl"><span class="gi">+    useInMemoryPgpKeys(signingKey, signingPassword)
</span></span></span><span class="line"><span class="cl"><span class="gi">+  }
</span></span></span><span class="line"><span class="cl"><span class="gi">+ }
</span></span></span></code></pre></div></blockquote>
<p>This will populate your POM files with the properties required by Maven Central and sign all artifacts with PGP.</p>
<h2 id="consuming-your-new-binary-plugins">Consuming your new binary plugins</h2>
<p>With your convention plugins converted to shiny new binary plugins, you might be inclined to start using them like so:</p>
<blockquote>
<p><code>autofill-parser/build.gradle.kts</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"> plugins {
</span></span><span class="line"><span class="cl"><span class="gd">-  id(&#34;com.github.android-password-store.published-android-library&#34;)
</span></span></span><span class="line"><span class="cl"><span class="gd">-  id(&#34;com.github.android-password-store.kotlin-android&#34;)
</span></span></span><span class="line"><span class="cl"><span class="gd">-  id(&#34;com.github.android-password-store.kotlin-library&#34;)
</span></span></span><span class="line"><span class="cl"><span class="gd">-  id(&#34;com.github.android-password-store.psl-plugin&#34;)
</span></span></span><span class="line"><span class="cl"><span class="gi">+  id(&#34;com.github.android-password-store.published-android-library&#34;) version &#34;1.0.0&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+  id(&#34;com.github.android-password-store.kotlin-android&#34;) version &#34;1.0.0&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+  id(&#34;com.github.android-password-store.kotlin-library&#34;) version &#34;1.0.0&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+  id(&#34;com.github.android-password-store.psl-plugin&#34;) version &#34;1.0.0&#34;
</span></span></span><span class="line"><span class="cl"> }
</span></span></code></pre></div></blockquote>
<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>
<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>
<blockquote>
<p><code>settings.gradle.kts</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gu">@@ -14,6 +14,25 @@ pluginManagement {
</span></span></span><span class="line"><span class="cl">     mavenCentral()
</span></span><span class="line"><span class="cl">     gradlePluginPortal()
</span></span><span class="line"><span class="cl">   }
</span></span><span class="line"><span class="cl"><span class="gi">+  plugins {
</span></span></span><span class="line"><span class="cl"><span class="gi">+    id(&#34;com.github.android-password-store.kotlin-android&#34;) version &#34;1.0.0&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+    id(&#34;com.github.android-password-store.kotlin-library&#34;) version &#34;1.0.0&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+    id(&#34;com.github.android-password-store.psl-plugin&#34;) version &#34;1.0.0&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+    id(&#34;com.github.android-password-store.published-android-library&#34;) version &#34;1.0.0&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+  }
</span></span></span><span class="line"><span class="cl"> }
</span></span></code></pre></div></blockquote>
<p>And you&rsquo;re off to the races!</p>
<h2 id="closing-notes">Closing notes</h2>
<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&rsquo;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>
]]></content:encoded>
    </item>
    
    <item>
      <title>Backing up your content from Google Photos</title>
      <link>https://msfjarvis.dev/posts/backing-up-your-content-from-google-photos/</link>
      <pubDate>Mon, 04 Apr 2022 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/backing-up-your-content-from-google-photos/</guid>
      <description>Putting your media into Google Photos is easy, taking it out, not as much.</description>
      <content:encoded><![CDATA[<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>
<p>There are many good reasons why you&rsquo;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>
<h3 id="how-to-archive-your-images-from-google-photos">How to archive your images from Google Photos</h3>
<ol>
<li>
<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>
</li>
<li>
<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&rsquo;re trying archive a few years of history.</p>
</li>
</ol>
<p>So, what&rsquo;s the solution?</p>
<h3 id="gphotos-cdp">gphotos-cdp</h3>
<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&rsquo;d take as a human to ensure you get copies of your pictures with all the EXIF metadata retained.</p>
<h3 id="setting-up-gphotos-cdp">Setting up gphotos-cdp</h3>
<blockquote>
<p>Disclaimer: I&rsquo;ve only tested this on Linux. This <em>should</em> be doable on other platforms, but it&rsquo;s not relevant to my needs so I will not be investigating that.</p>
</blockquote>
<p>Ideally you&rsquo;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>
<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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">google-chrome <span class="se">\
</span></span></span><span class="line"><span class="cl">  --user-data-dir<span class="o">=</span>gphotos-cdp <span class="se">\
</span></span></span><span class="line"><span class="cl">  --no-first-run  <span class="se">\
</span></span></span><span class="line"><span class="cl">  --password-store<span class="o">=</span>basic <span class="se">\
</span></span></span><span class="line"><span class="cl">  --use-mock-keychain <span class="se">\
</span></span></span><span class="line"><span class="cl">  https://photos.google.com/
</span></span></code></pre></div><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>
<blockquote>
<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>
</blockquote>
<p>Once done, you&rsquo;ll have a <code>gphotos-cdp</code> directory that you&rsquo;ll need to move to the <code>/tmp</code> directory of whichever machine you wish to run gphotos-cdp on.</p>
<p>gphotos-cdp is written in <a href="https://go.dev">Golang</a> so you&rsquo;ll need to install it first. Once done, run the following command to install the latest version of gphotos-cdp</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">go install github.com/perkeep/gphotos-cdp@latest
</span></span></code></pre></div><p>Then you can go ahead and start using gphotos-cdp, as given below</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">~/go/bin/gphotos-cdp <span class="se">\ </span><span class="c1"># go install puts things in ~/go/bin by default</span>
</span></span><span class="line"><span class="cl">  -v <span class="se">\ </span><span class="c1"># Enable verbose logging</span>
</span></span><span class="line"><span class="cl">  -dev <span class="se">\ </span><span class="c1"># Enable dev mode which always uses /tmp/gphotos-cdp as the profile directory</span>
</span></span><span class="line"><span class="cl">  -headless <span class="se">\ </span><span class="c1"># Run Chrome in headless mode so it works on servers and such</span>
</span></span><span class="line"><span class="cl">  -dldir ~/photos <span class="c1"># Download everything to ~/photos</span>
</span></span></code></pre></div><p>The automation techniques used are not completely reliable and can often fail. You&rsquo;ll want to implement some kind of retry-on-failure logic to ensure this is run a few times every day.</p>
<h3 id="monitoring">Monitoring</h3>
<p>With anything built on such a brittle foundation, it&rsquo;s useful to be able to constantly monitor that things are working as they should.</p>
<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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">HEALTHCHECKS_CHECK_ID</span><span class="o">=</span>&lt;UUID <span class="k">for</span> check as given on healthchecks&gt; <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="nv">HEALTHCHECKS_USERAGENT</span><span class="o">=</span>crontab <span class="se">\
</span></span></span><span class="line"><span class="cl">~/bin/healthchecks-monitor --retries <span class="m">3</span> <span class="se">\ </span><span class="c1"># Try running the command thrice before giving up</span>
</span></span><span class="line"><span class="cl">  --timer <span class="se">\ </span><span class="c1"># Start off a server-side timer on healthchecks</span>
</span></span><span class="line"><span class="cl">  --logs <span class="se">\ </span><span class="c1"># Record execution logs on healthchecks in case of failure, to help with debugging</span>
</span></span><span class="line"><span class="cl">  --exec <span class="s2">&#34;~/go/bin/gphotos-cdp -v -dev -headless -dldir ~/photos&#34;</span>
</span></span></code></pre></div><h3 id="conclusion">Conclusion</h3>
<p>As evident, it&rsquo;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 &ldquo;device&rdquo; 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>
<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>
<p>Hopefully this setup causes you minimal grief and allows you to back up your precious memories without relying only on Google :)</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Migrating APS to Material You</title>
      <link>https://msfjarvis.dev/posts/migrating-aps-to-material-you/</link>
      <pubDate>Sat, 06 Nov 2021 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/migrating-aps-to-material-you/</guid>
      <description>I recently migrated Password Store to Material You, Google&amp;rsquo;s latest iteration of Material Design. Here&amp;rsquo;s how it went.</description>
      <content:encoded><![CDATA[<p>With much fanfare, Google released the next iteration of Material Design: <strong>Material You</strong>. It&rsquo;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&rsquo;s what prompted me to update APS to Material You and join in :)</p>
<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>
<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>
<h2 id="getting-the-basics-in">Getting the basics in</h2>
<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&rsquo;s the &ldquo;<a href="https://material.io/blog/migrating-material-3">Migration to Material 3</a>&rdquo; 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&rsquo;s View-based system. These were extremely helpful in getting a headstart on the whole process. It&rsquo;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&rsquo;t hurt.</p>
<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&rsquo;ve previously used and found to be excellent for visual appeal and accessibility. I&rsquo;m still not a 100% confident in my choice, so if people have better options in mind I&rsquo;d love to know down in the comments.</p>
<p>Once the new font face was in, I opted to enable dynamic colors. Admittedly not the right choice, since I should&rsquo;ve validated the &ldquo;default&rdquo; palette first but that&rsquo;s what I did ¯\<em>(ツ)</em>/¯.</p>
<h2 id="bugfixes-and-improvements">Bugfixes and improvements™️</h2>
<p>Once the M3 themes were all prepped, it was time to actually start migrating.</p>
<p>I switched our activities to use the M3 themes, and immediately started noticing bugs from non-idiomatic and straight up incorrect theming we&rsquo;ve been lugging around for the past couple years.</p>
<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>
<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>
<p>There were a lot more smaller changes that were made to address the remaining visual bugs</p>
<ul>
<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>
<li>A lot of screens were using hard-coded colors, which were migrated to theme attributes</li>
<li>Many screens used hard-coded styles for buttons and text fields, and were also migrated to theme attributes</li>
<li>Multiple layouts also referenced typography styles directly and were migrated to the corresponding M3 attributes, based on the mapping table in the &ldquo;<a href="https://material.io/blog/migrating-material-3">Migration to Material 3</a>&rdquo; article.</li>
<li>System bars and Toolbar had to be given explicit styles and colors to match the &ldquo;flat&rdquo; aesthetic from our M2 designs.</li>
</ul>
<h2 id="the-final-stretch">The final stretch</h2>
<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>
<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>
<h2 id="screenshots">Screenshots!</h2>
<h3 id="before">Before</h3>
<p><img alt="Screenshot gallery of a few APS screens before the Material 3 migration" loading="lazy" src="/posts/migrating-aps-to-material-you/aps_m2_gallery.webp"></p>
<h3 id="after">After</h3>
<p><img alt="Screenshot gallery of a few APS screens after the Material 3 migration" loading="lazy" src="/posts/migrating-aps-to-material-you/aps_m3_gallery.webp"></p>
<h2 id="closing-notes">Closing notes</h2>
<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&rsquo;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>
<p>I&rsquo;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&rsquo;t for the tooling and guidance from the team.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Building static Rust binaries for Linux</title>
      <link>https://msfjarvis.dev/posts/building-static-rust-binaries-for-linux/</link>
      <pubDate>Sun, 17 Oct 2021 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/building-static-rust-binaries-for-linux/</guid>
      <description>Some tips on building static binaries of Rust projects targeting Linux</description>
      <content:encoded><![CDATA[<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
has 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>
<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&rsquo;ll run into an error similar to this:</p>
<pre tabindex="0"><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
</code></pre><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 &ldquo;fix&rdquo; 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>
<p>Therefore, the correct way to build a statically linked glibc executable for an x86_64 machine is this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nv">RUSTFLAGS</span><span class="o">=</span><span class="s1">&#39;-C target-feature=+crt-static&#39;</span> cargo build --release --target x86_64-unknown-linux-gnu
</span></span></code></pre></div><h2 id="other-potential-problems">Other potential problems</h2>
<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>&rsquo;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>&rsquo;s <code>v4.1.0</code> release gives us this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ cargo tree <span class="p">|</span> rg -- -sys
</span></span><span class="line"><span class="cl">│   │   │   │   ├── curl-sys v0.4.45+curl-7.78.0
</span></span><span class="line"><span class="cl">│   │   │   │   │   ├── libnghttp2-sys v0.1.6+1.43.0
</span></span><span class="line"><span class="cl">│   │   │   │   │   ├── libz-sys v1.1.3
</span></span><span class="line"><span class="cl">│   │   │   │   │   └── openssl-sys v0.9.66
</span></span><span class="line"><span class="cl">│   │   │   │   ├── openssl-sys v0.9.66 <span class="o">(</span>*<span class="o">)</span>
</span></span><span class="line"><span class="cl">│   │   │   ├── curl-sys v0.4.45+curl-7.78.0 <span class="o">(</span>*<span class="o">)</span>
</span></span><span class="line"><span class="cl">│   └── web-sys v0.3.53
</span></span><span class="line"><span class="cl">│       ├── js-sys v0.3.53
</span></span></code></pre></div><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>&quot;curl_client&quot;</code> feature by default, which can be turned off and replaced with <code>&quot;h1-client-rustls&quot;</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>
]]></content:encoded>
    </item>
    
    <item>
      <title>Learning Zig - Day 4</title>
      <link>https://msfjarvis.dev/posts/learning-zig--day-4/</link>
      <pubDate>Tue, 18 May 2021 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/learning-zig--day-4/</guid>
      <description>Brushing up on standards</description>
      <content:encoded><![CDATA[<p>Today I&rsquo;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>
<h1 id="thoughts">Thoughts™️</h1>
<h2 id="allocators">Allocators</h2>
<p>Rust&rsquo;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>
<p>I&rsquo;ve never given an <em>extreme</em> amount of thought to the allocations my programs perform, so I&rsquo;m rather unopinionated on this. That being said, whenever I start working on the healthchecks library I&rsquo;m definitely going to follow the ecosystem&rsquo;s practices and make allocations explicit for consumers.</p>
<h2 id="filesystem">Filesystem</h2>
<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&rsquo;m pretty sure I&rsquo;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&rsquo;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&rsquo;s education system :P</p>
<h2 id="formatting">Formatting</h2>
<p>Finally an answer to my &ldquo;how to format an array&rdquo; mystery! It&rsquo;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>
<h2 id="json">JSON</h2>
<p>The native JSON support looks pretty great, though I am left to wonder how much of <a href="https://serde.rs/">serde</a>&rsquo;s flexibility is available here. Guess we&rsquo;ll find out soon enough :)</p>
<h2 id="random-numbers-and-crypto">Random numbers and crypto</h2>
<p>Great to see native crypto in Zig! I&rsquo;m not the target audience, but lack of crypto primitives in Rust seems to come up often so it&rsquo;s a nice plus for Zig.</p>
<h2 id="formatting-specifiers-and-advanced-formatting">Formatting specifiers and Advanced Formatting</h2>
<p>Zig&rsquo;s formatting system seems about as powerful as Rust&rsquo;s, <em>maybe more</em>, so it&rsquo;s definitely a plus for me since I&rsquo;ve ended up debugging a lot of code with <code>println!(&quot;{:?}&quot;, $field)</code> in Rust 😅</p>
<h1 id="conclusion">Conclusion</h1>
<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>
<p>Overall, I&rsquo;m liking everything I&rsquo;m seeing. Very excited to start building things in Zig!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Learning Zig - Day 3</title>
      <link>https://msfjarvis.dev/posts/learning-zig--day-3/</link>
      <pubDate>Sun, 16 May 2021 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/learning-zig--day-3/</guid>
      <description>Finishing up the basics</description>
      <content:encoded><![CDATA[<p><a href="/posts/learning-zig--day-2">Yesterday&rsquo;s post</a> was a bit shorter than I planned, since I didn&rsquo;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&rsquo;ll be wrapping it up.</p>
<h1 id="thoughts">Thoughts™️</h1>
<p>Same as yesterday, this section will be a brain dump of what I think of the things I learn today.</p>
<h2 id="pointers">Pointers</h2>
<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&rsquo;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&rsquo;m sure I&rsquo;ll get used to it in no time.</p>
<p>Just like Rust, mutable and immutable pointers are explicitly distinct which is a ✅ in my book.</p>
<p>There wasn&rsquo;t a lot of content on ZigLearn about <a href="https://ziglearn.org/chapter-1/#many-item-pointers">many-item pointers</a> so I&rsquo;m still not sure I understand any of it. That&rsquo;s probably just me though.</p>
<p>I&rsquo;ve known and used Rust&rsquo;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>
<h2 id="enums">Enums</h2>
<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>
<h2 id="structs">Structs</h2>
<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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-zig" data-lang="zig"><span class="line"><span class="cl"><span class="kr">const</span><span class="w"> </span><span class="n">Rectangle</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">length</span><span class="p">:</span><span class="w"> </span><span class="kt">i32</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">width</span><span class="p">:</span><span class="w"> </span><span class="kt">i32</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">pub</span><span class="w"> </span><span class="k">fn</span><span class="w"> </span><span class="nf">swap</span><span class="p">(</span><span class="n">self</span><span class="p">:</span><span class="w"> </span><span class="o">*</span><span class="n">Rectangle</span><span class="p">)</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// No explicit dereferencing needed!</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kr">const</span><span class="w"> </span><span class="n">tmp</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">self</span><span class="p">.</span><span class="n">length</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">self</span><span class="p">.</span><span class="n">length</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">self</span><span class="p">.</span><span class="n">width</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">self</span><span class="p">.</span><span class="n">width</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">tmp</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">};</span><span class="w">
</span></span></span></code></pre></div><h2 id="unions">Unions</h2>
<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&rsquo;s <a href="https://kotlinlang.org/docs/sealed-classes.html">sealed classes</a> functionality so I&rsquo;m looking forward to writing some cursed code :D</p>
<h2 id="integer-rules-and-floats">Integer rules and Floats</h2>
<p>Zig&rsquo;s type coercion syntax is nicer than Rust&rsquo;s, though the lack of a runtime error concerned me initially. Rust&rsquo;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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-zig" data-lang="zig"><span class="line"><span class="cl"><span class="k">fn</span><span class="w"> </span><span class="nf">returnsNum</span><span class="p">()</span><span class="w"> </span><span class="kt">u32</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="mi">1_00_000</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">test</span><span class="w"> </span><span class="s">&#34;typecast&#34;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">testing</span><span class="p">.</span><span class="nf">expect</span><span class="p">(</span><span class="nb">@TypeOf</span><span class="p">(</span><span class="nb">@as</span><span class="p">(</span><span class="kt">u8</span><span class="p">,</span><span class="w"> </span><span class="nf">returnsNum</span><span class="p">()))</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="kt">u8</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>The build will fail with:</p>
<pre tabindex="0"><code>./src/main.zig:182:46: error: expected type &#39;u8&#39;, found &#39;u32&#39;
    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);
</code></pre><p>This is good, but I haven&rsquo;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>
<h2 id="optionals">Optionals</h2>
<p>Zig&rsquo;s <a href="https://ziglang.org/documentation/master/#Optionals">Optionals</a> are a very good parallel for Rust&rsquo;s <a href="https://doc.rust-lang.org/std/option/enum.Option.html">Option</a>, though Zig provides a lot more syntactic niceties.</p>
<p>The fact that you can use a while loop to capture values until they become null is pretty damn sweet.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-zig" data-lang="zig"><span class="line"><span class="cl"><span class="kr">var</span><span class="w"> </span><span class="n">numbers_left</span><span class="p">:</span><span class="w"> </span><span class="kt">u32</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">4</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">fn</span><span class="w"> </span><span class="nf">eventuallyNullSequence</span><span class="p">()</span><span class="w"> </span><span class="p">?</span><span class="kt">u32</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">numbers_left</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="kc">null</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">numbers_left</span><span class="w"> </span><span class="o">-=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">numbers_left</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">test</span><span class="w"> </span><span class="s">&#34;while null capture&#34;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">var</span><span class="w"> </span><span class="n">sum</span><span class="p">:</span><span class="w"> </span><span class="kt">u32</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">while</span><span class="w"> </span><span class="p">(</span><span class="nf">eventuallyNullSequence</span><span class="p">())</span><span class="w"> </span><span class="o">|</span><span class="n">value</span><span class="o">|</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">sum</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="n">value</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nf">expect</span><span class="p">(</span><span class="n">sum</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">6</span><span class="p">);</span><span class="w"> </span><span class="c1">// 3 + 2 + 1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h1 id="conclusion">Conclusion</h1>
<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>
<p><a href="https://ziglearn.org/chapter-2/">Chapter 2</a> introduces JSON, which we&rsquo;ll need for our eventual healthchecks.io library, so I&rsquo;m looking forward to it! I do have work tomorrow, so we&rsquo;ll have to see if I can keep up the daily streak :)</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Learning Zig - Day 2</title>
      <link>https://msfjarvis.dev/posts/learning-zig--day-2/</link>
      <pubDate>Sat, 15 May 2021 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/learning-zig--day-2/</guid>
      <description>Onwards in our quest to learn Zig</description>
      <content:encoded><![CDATA[<p>In the <a href="/posts/first-steps-with-zig">previous post</a> I documented how I went about setting up my Zig environment, and it&rsquo;s now time to start learning things.</p>
<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&rsquo;ve elected to rebuild my <a href="https://msfjarvis.dev/g/healthchecks-rs">healthchecks-rs</a> library. It&rsquo;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>
<h1 id="getting-the-basics-down">Getting the basics down</h1>
<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 &ldquo;checkpoints&rdquo;, much like the <a href="https://doc.rust-lang.org/book/">Rust book</a>.</p>
<p>For this post I&rsquo;m going through <a href="https://ziglearn.org/chapter-1/">chapter 1</a>.</p>
<h1 id="thoughts">Thoughts™️</h1>
<p>I&rsquo;m going to use this section to jot down my thoughts about Zig, broken down by the sections on ZigLearn. I&rsquo;ll skip the parts that I don&rsquo;t have anything to say on.</p>
<h2 id="assignment">Assignment</h2>
<p>The presence of <code>undefined</code> is <em>very</em> interesting to me. It appears to be functionally identical to Rust&rsquo;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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-zig" data-lang="zig"><span class="line"><span class="cl"><span class="kr">const</span><span class="w"> </span><span class="n">std</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">@import</span><span class="p">(</span><span class="s">&#34;std&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kr">const</span><span class="w"> </span><span class="n">Vec3</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">x</span><span class="p">:</span><span class="w"> </span><span class="kt">f32</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">y</span><span class="p">:</span><span class="w"> </span><span class="kt">f32</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">z</span><span class="p">:</span><span class="w"> </span><span class="kt">f32</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">};</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kr">pub</span><span class="w"> </span><span class="k">fn</span><span class="w"> </span><span class="nf">main</span><span class="p">()</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">inferred_constant</span><span class="p">:</span><span class="w"> </span><span class="n">Vec3</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">undefined</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">std</span><span class="p">.</span><span class="n">debug</span><span class="p">.</span><span class="nf">print</span><span class="p">(</span><span class="s">&#34;Hello, {d}!</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span><span class="w"> </span><span class="p">.{</span><span class="n">inferred_constant</span><span class="p">});</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>which prints:</p>
<pre tabindex="0"><code>➜ zig build run
Hello, Vec3{ .x = 0, .y = 0, .z = 0 }!
</code></pre><p>I have clearly not gotten very far yet, but initial thoughts here: Rust&rsquo;s trait-based implementation means I can customize the &ldquo;default&rdquo; values for my structs, which I&rsquo;m not seeing in this implicit coercion yet. Guess we&rsquo;ll find out soon whether or not this can be handled explicitly :D</p>
<h2 id="arrays">Arrays</h2>
<p>The syntax for array declarations is quite clear and explicit, which I like. Notably, while you can do this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-zig" data-lang="zig"><span class="line"><span class="cl"><span class="kr">const</span><span class="w"> </span><span class="n">implicitly_sized_array</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">_</span><span class="p">]</span><span class="kt">u8</span><span class="p">{};</span><span class="w"> </span><span class="c1">// _ means &#34;infer the size&#34;</span><span class="w">
</span></span></span></code></pre></div><p>you cannot have an inferred size reference as the type:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-zig" data-lang="zig"><span class="line"><span class="cl"><span class="kr">const</span><span class="w"> </span><span class="n">implicitly_sized_array</span><span class="p">[</span><span class="n">_</span><span class="p">]</span><span class="kt">u8</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{};</span><span class="w">
</span></span></span></code></pre></div><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&rsquo;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>
<p>The only problem I encountered here was that I can&rsquo;t figure out how to print an array!</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-zig" data-lang="zig"><span class="line"><span class="cl"><span class="kr">const</span><span class="w"> </span><span class="n">implicitly_sized_array</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">_</span><span class="p">]</span><span class="kt">u8</span><span class="p">{</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">3</span><span class="p">};</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">std</span><span class="p">.</span><span class="n">debug</span><span class="p">.</span><span class="nf">print</span><span class="p">(</span><span class="s">&#34;This is an array: {}</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span><span class="w"> </span><span class="p">.{</span><span class="n">implicitly_sized_array</span><span class="p">});</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// Outputs: &#34;This is an array: &#34;</span><span class="w">
</span></span></span></code></pre></div><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&rsquo;t work. Hopefully this&rsquo;ll get cleared up later.</p>
<h2 id="if">If</h2>
<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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">Test <span class="o">[</span>2/2<span class="o">]</span> <span class="nb">test</span> <span class="s2">&#34;while with continue expression&#34;</span>... expected 2080, found <span class="m">10</span>
</span></span><span class="line"><span class="cl">/nix/store/nhd75c4sr3l9wlaspilkwawx5ixkn74w-zig-0.7.1/lib/zig/std/testing.zig:74:32: 0x206f9a in std.testing.expectEqual <span class="o">(</span><span class="nb">test</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">                std.debug.panic<span class="o">(</span><span class="s2">&#34;expected {}, found {}&#34;</span>, .<span class="o">{</span> expected, actual <span class="o">})</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                               ^
</span></span><span class="line"><span class="cl">/home/msfjarvis/git-repos/zig-playground/src/main.zig:29:24: 0x205abd in <span class="nb">test</span> <span class="s2">&#34;while with continue expression&#34;</span> <span class="o">(</span><span class="nb">test</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">    testing.expectEqual<span class="o">(</span>sum, 10<span class="o">)</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                       ^
</span></span><span class="line"><span class="cl">/nix/store/nhd75c4sr3l9wlaspilkwawx5ixkn74w-zig-0.7.1/lib/zig/std/special/test_runner.zig:61:28: 0x22e161 in std.special.main <span class="o">(</span><span class="nb">test</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">        <span class="o">}</span> <span class="k">else</span> test_fn.func<span class="o">()</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                           ^
</span></span><span class="line"><span class="cl">/nix/store/nhd75c4sr3l9wlaspilkwawx5ixkn74w-zig-0.7.1/lib/zig/std/start.zig:334:37: 0x20749d in std.start.posixCallMainAndExit <span class="o">(</span><span class="nb">test</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">            const <span class="nv">result</span> <span class="o">=</span> root.main<span class="o">()</span> catch <span class="p">|</span>err<span class="p">|</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">                                    ^
</span></span><span class="line"><span class="cl">/nix/store/nhd75c4sr3l9wlaspilkwawx5ixkn74w-zig-0.7.1/lib/zig/std/start.zig:162:5: 0x2071d2 in std.start._start <span class="o">(</span><span class="nb">test</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">    @call<span class="o">(</span>.<span class="o">{</span> .modifier <span class="o">=</span> .never_inline <span class="o">}</span>, posixCallMainAndExit, .<span class="o">{})</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    ^
</span></span><span class="line"><span class="cl">error: the following <span class="nb">test</span> <span class="nb">command</span> crashed:
</span></span><span class="line"><span class="cl">./src/zig-cache/o/5a647f3d3394214b30b3861a6b0ffbbb/test
</span></span></code></pre></div><h2 id="defer">Defer</h2>
<p>The <code>defer</code> language feature is something I&rsquo;ve always been curious about since seeing it in Go, so I&rsquo;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>
<h2 id="errors">Errors</h2>
<p>I like how easy it is to define errors, but the syntax feels kinda icky. Having each error enum I declare &lsquo;magically&rsquo; become a property on the <code>error</code> keyword doesn&rsquo;t sit right with me :(</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-zig" data-lang="zig"><span class="line"><span class="cl"><span class="kr">const</span><span class="w"> </span><span class="n">NumericError</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">error</span><span class="p">{};</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">fn</span><span class="w"> </span><span class="nf">mayError</span><span class="p">(</span><span class="n">shouldError</span><span class="p">:</span><span class="w"> </span><span class="kt">bool</span><span class="p">)</span><span class="w"> </span><span class="kt">anyerror</span><span class="o">!</span><span class="kt">u32</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">shouldError</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// This is different from what I&#39;m accustomed to as a user of</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// Either/Result type monads.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">error</span><span class="p">.</span><span class="n">NumericError</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">else</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="mi">10</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h2 id="runtime-safety">Runtime Safety</h2>
<p>Being able to turn off runtime safety features (like bounds checking) in specific blocks is pretty interesting! Not sure if I&rsquo;ll ever have a valid use for it though&hellip;</p>
<h1 id="conclusion">Conclusion</h1>
<p>I like most of what I&rsquo;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&rsquo;ve kinda come to expect it everywhere based on my previous experiences with Python, Java, Kotlin, and Rust; but maybe I&rsquo;ll now learn to appreciate how every character on my screen is just numbers :D.</p>
<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>
]]></content:encoded>
    </item>
    
    <item>
      <title>First steps with Zig</title>
      <link>https://msfjarvis.dev/posts/first-steps-with-zig/</link>
      <pubDate>Fri, 14 May 2021 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/first-steps-with-zig/</guid>
      <description>I&amp;rsquo;ve decided to learn Zig, and here&amp;rsquo;s how I&amp;rsquo;m preparing for it.</description>
      <content:encoded><![CDATA[<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>
<h1 id="compiler-and-ide-setup">Compiler and IDE setup</h1>
<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&rsquo;s ~6 months which means they are often rendered outdated by the fast pace of Zig development.</p>
<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>
<p>A quick <code>nix-shell -p zig</code> later, I now had access to the Zig compiler.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">➜ nix-shell -p zig
</span></span><span class="line"><span class="cl">➜ zig version
</span></span><span class="line"><span class="cl">0.7.1
</span></span></code></pre></div><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&rsquo;m not sure if that&rsquo;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>
<h1 id="learning-resources">Learning resources</h1>
<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>
<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>
<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&rsquo;ll see Zig be able to devote effort towards this as well.</p>
<h1 id="your-first-program">Your first program</h1>
<p>The <code>zig</code> CLI contains commands to generate new projects easily, so let&rsquo;s create a new binary project.</p>
<pre tabindex="0"><code>➜ zig init-exe
info: Created build.zig
info: Created src/main.zig
info: Next, try `zig build --help` or `zig build run`
</code></pre><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&rsquo;s what <code>zig init-exe</code> gives you for a &ldquo;hello world&rdquo; program:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-zig" data-lang="zig"><span class="line"><span class="cl"><span class="c1">// src/main.zig</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kr">const</span><span class="w"> </span><span class="n">std</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">@import</span><span class="p">(</span><span class="s">&#34;std&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kr">pub</span><span class="w"> </span><span class="k">fn</span><span class="w"> </span><span class="nf">main</span><span class="p">()</span><span class="w"> </span><span class="kt">anyerror</span><span class="o">!</span><span class="kt">void</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">std</span><span class="p">.</span><span class="n">log</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="s">&#34;All your codebase are belong to us.&#34;</span><span class="p">,</span><span class="w"> </span><span class="p">.{});</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Cheeky.</p>
<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>
]]></content:encoded>
    </item>
    
    <item>
      <title>Tips and Tricks for GitHub Actions</title>
      <link>https://msfjarvis.dev/posts/github-actions-tips-tricks/</link>
      <pubDate>Sat, 02 Jan 2021 05:30:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/github-actions-tips-tricks/</guid>
      <description>GitHub Actions is a power CI/CD platform that can do a lot more than your traditional CI systems. Here&amp;rsquo;s some tips to get you started with exploring its true potential.</description>
      <content:encoded><![CDATA[<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&rsquo;s pricing for open source is certainly bound to accelerate this even more.</p>
<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>
<p>Let&rsquo;s go over some things that you can do with Actions, and we&rsquo;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>
<h2 id="running-workflows-based-on-a-cron-trigger">Running workflows based on a cron trigger</h2>
<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&rsquo;s see how we can use the schedule feature to automate repetitive tasks.</p>
<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 &lsquo;base&rsquo; domain of the website we&rsquo;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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Update Publix Suffix List data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">schedule</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">cron</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;0 0 * * 6&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">update-publicsuffix-data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># The actual workflow doing the update job</span><span class="w">
</span></span></span></code></pre></div><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>
<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>
<h2 id="running-jobs-based-on-commit-message">Running jobs based on commit message</h2>
<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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy to Cloudflare Workers Sites</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">main</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">deploy-main</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;contains(github.event.head_commit.message, &#39;[deploy]&#39;)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># Set up wrangler and push to the production environment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">deploy-staging</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;contains(github.event.head_commit.message, &#39;[staging]&#39;)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># Set up wrangler and push to the staging environment</span><span class="w">
</span></span></span></code></pre></div><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&rsquo;t need to be reflected in the staging environment.</p>
<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>
<h2 id="testing-across-multiple-configurations-in-parallel">Testing across multiple configurations in parallel</h2>
<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>
<table>
  <thead>
      <tr>
          <th></th>
          <th>Windows</th>
          <th>MacOS</th>
          <th>Ubuntu</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Stable</td>
          <td>Windows + Stable</td>
          <td>MacOS + Stable</td>
          <td>Ubuntu + Stable</td>
      </tr>
      <tr>
          <td>Beta</td>
          <td>Windows + Beta</td>
          <td>MacOS + Beta</td>
          <td>Ubuntu + Beta</td>
      </tr>
      <tr>
          <td>Nightly</td>
          <td>Windows + Nightly</td>
          <td>MacOS + Nightly</td>
          <td>Ubuntu + Nightly</td>
      </tr>
  </tbody>
</table>
<blockquote>
<sub>This particular matrix is for Rust, to test a codebase across Windows, Ubuntu, and macOS using the Rust stable, beta, and nightly toolchains.</sub>

</blockquote>
<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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">check-rust-code</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">strategy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># Defines a matrix strategy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">matrix</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># Sets the OSes we want to run jobs on</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">os</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">ubuntu-latest, windows-latest, macOS-latest]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># Sets the Rust channels we want to test against</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">rust</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">stable, beta, nightly]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># Make the job run on the OS picked by the matrix</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">${{ matrix.os }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions-rs/toolchain@v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">profile</span><span class="p">:</span><span class="w"> </span><span class="l">minimal</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">components</span><span class="p">:</span><span class="w"> </span><span class="l">rustfmt, clippy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="c"># Installs the Rust toolchain for the channel picked by the matrix</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">toolchain</span><span class="p">:</span><span class="w"> </span><span class="l">${{ matrix.rust }}</span><span class="w">
</span></span></span></code></pre></div><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>
<h2 id="make-a-job-run-after-another">Make a job run after another</h2>
<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&rsquo;s try another real world example!</p>
<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&rsquo;t be a snapshot deployment until all tests are passing on the main branch. The relevant parts of the workflow configuration are here:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pull_request</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">main</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">checks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Runs automated unit and instrumentation tests</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">snapshot-deployment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Only run if the push event triggered this workflow run</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;github.event_name == &#39;push&#39;&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Run after the &#39;checks&#39; job has passed</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">checks]</span><span class="w">
</span></span></span></code></pre></div><h1 id="mitigating-security-concerns-with-actions">Mitigating security concerns with Actions</h1>
<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&rsquo;m going to outline them here. I&rsquo;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>
<h2 id="use-exact-commit-hashes-rather-than-tags">Use exact commit hashes rather than tags</h2>
<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>
<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>
<p><img alt="A tag along with its commit hash" loading="lazy" src="/posts/github-actions-tips-tricks/actions_tips_tricks_commit_hash.webp"></p>
<blockquote>
<sub>Here, the commit hash is feb985e. Ideally, you want to click that link and copy the full hash from the URL</sub>

</blockquote>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl">job:
</span></span><span class="line"><span class="cl">  checks:
</span></span><span class="line"><span class="cl"><span class="gd">-   - uses: burrunan/gradle-cache-actions@v1.6
</span></span></span><span class="line"><span class="cl"> +  - uses: burrunan/gradle-cache-actions@feb985ecf49f57f54f31920821a50d0394faf122
</span></span></code></pre></div><h3 id="alternate-solution">Alternate solution</h3>
<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&rsquo;d be available in a single PR diff.</p>
<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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl">job:
</span></span><span class="line"><span class="cl">  checks:
</span></span><span class="line"><span class="cl">    - name: Checkout repository
</span></span><span class="line"><span class="cl">    # Assuming the copy of actions/checkout is at .github/actions/checkout
</span></span><span class="line"><span class="cl"><span class="gd">-   - uses: actions/checkout@v2
</span></span></span><span class="line"><span class="cl"><span class="gi">+   - uses: ./.github/actions/checkout
</span></span></span></code></pre></div><h2 id="replace-pull_request_target-with-pull_request">Replace <code>pull_request_target</code> with <code>pull_request</code></h2>
<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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">-on: [push, pull_request_target]
</span></span></span><span class="line"><span class="cl"><span class="gi">+on: [push, pull_request]
</span></span></span></code></pre></div><hr />

<p>I&rsquo;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>
]]></content:encoded>
    </item>
    
    <item>
      <title>Manually parsing JSON with Moshi</title>
      <link>https://msfjarvis.dev/posts/manually-parsing-json-with-moshi/</link>
      <pubDate>Mon, 21 Dec 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/manually-parsing-json-with-moshi/</guid>
      <description>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</description>
      <content:encoded><![CDATA[<h3 id="what-is-moshi">What is Moshi?</h3>
<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&rsquo;s Gson to address some of its shortcomings and to have an alternative that was actively maintained.</p>
<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&rsquo;s recommended to use the codegen method if possible.</p>
<h3 id="what-is-an-adapter">What is an adapter?</h3>
<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&rsquo;s the example Moshi uses</a>, and should be all the introduction you need for this particular type.</p>
<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&rsquo;re going to try to build.</p>
<h3 id="why-write-your-own-adapters">Why write your own adapters?</h3>
<p>Good question. Consider this example class:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="nd">@JsonClass</span><span class="p">(</span><span class="n">generateAdapter</span> <span class="p">=</span> <span class="k">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">TextParts</span><span class="p">(</span><span class="k">val</span> <span class="py">heading</span><span class="p">:</span> <span class="n">String</span><span class="p">,</span> <span class="k">val</span> <span class="py">body</span><span class="p">:</span> <span class="n">String</span><span class="p">?</span> <span class="p">=</span> <span class="k">null</span><span class="p">)</span>
</span></span></code></pre></div><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&rsquo;s try converting this to JSON.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="k">val</span> <span class="py">text</span> <span class="p">=</span> <span class="n">TextParts</span><span class="p">(</span><span class="s2">&#34;This is the heading&#34;</span><span class="p">,</span> <span class="s2">&#34;And this is the body&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">val</span> <span class="py">moshi</span> <span class="p">=</span> <span class="nc">Moshi</span><span class="p">.</span><span class="n">Builder</span><span class="p">().</span><span class="n">build</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="c1">// TextPartsJsonAdapter was generated by the codegen backend
</span></span></span><span class="line"><span class="cl"><span class="n">println</span><span class="p">(</span><span class="n">TextPartsJsonAdapter</span><span class="p">(</span><span class="n">moshi</span><span class="p">).</span><span class="n">toJson</span><span class="p">(</span><span class="n">text</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span><span class="s2">&#34;heading&#34;</span><span class="p">:</span><span class="s2">&#34;This is the heading&#34;</span><span class="p">,</span><span class="s2">&#34;body&#34;</span><span class="p">:</span><span class="s2">&#34;And this is the body&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>What this means is, given a JSON object that looks like this</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span> <span class="nt">&#34;heading&#34;</span><span class="p">:</span> <span class="s2">&#34;This is the heading&#34;</span><span class="p">,</span> <span class="nt">&#34;body&#34;</span><span class="p">:</span> <span class="s2">&#34;And this is the body&#34;</span> <span class="p">}</span>
</span></span></code></pre></div><p>We can get an instance of <code>TextParts</code> that looks like this</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="k">val</span> <span class="py">text</span> <span class="p">=</span> <span class="n">TextParts</span><span class="p">(</span><span class="s2">&#34;This is the heading&#34;</span><span class="p">,</span> <span class="s2">&#34;And this is the body&#34;</span><span class="p">)</span>
</span></span></code></pre></div><p>Cool! Now, let&rsquo;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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;heading&#34;</span><span class="p">:</span> <span class="s2">&#34;This is the heading&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;extras&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;body&#34;</span><span class="p">:</span> <span class="s2">&#34;And this is the body&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="nd">@JsonClass</span><span class="p">(</span><span class="n">generateAdapter</span> <span class="p">=</span> <span class="k">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Extras</span><span class="p">(</span><span class="k">val</span> <span class="py">body</span><span class="p">:</span> <span class="n">String</span><span class="p">?</span> <span class="p">=</span> <span class="k">null</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@JsonClass</span><span class="p">(</span><span class="n">generateAdapter</span> <span class="p">=</span> <span class="k">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">TextParts</span><span class="p">(</span><span class="k">val</span> <span class="py">heading</span><span class="p">:</span> <span class="n">String</span><span class="p">,</span> <span class="k">val</span> <span class="py">extras</span><span class="p">:</span> <span class="n">Extras</span><span class="p">?</span> <span class="p">=</span> <span class="k">null</span><span class="p">)</span>
</span></span></code></pre></div><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&rsquo;t that nice. You&rsquo;re also now incurring the (albeit miniscule) overhead of generating two adapters rather than one. Wouldn&rsquo;t it be great if we could continue to have a flat object like before? Let&rsquo;s try to make that happen.</p>
<h3 id="how-to-write-your-own-moshi-adapter">How to write your own Moshi adapter?</h3>
<p>With less effort than one might think! Let&rsquo;s put down the basic building blocks.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">TextPartsJsonAdapter</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// Moshi is flexible about the parameters of these two methods, and for simpler types
</span></span></span><span class="line"><span class="cl">  <span class="c1">// you will find it easier to follow the example from the Moshi README which does not
</span></span></span><span class="line"><span class="cl">  <span class="c1">// use JsonReader/JsonWriter and instead directly converts items to and from their String
</span></span></span><span class="line"><span class="cl">  <span class="c1">// representations. The method names are also not enforced, as Moshi only uses the
</span></span></span><span class="line"><span class="cl">  <span class="c1">// annotations to find relevant methods. The internal implementation of how they do it
</span></span></span><span class="line"><span class="cl">  <span class="c1">// can be found here: https://git.io/JLwnb
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nd">@FromJson</span>
</span></span><span class="line"><span class="cl">  <span class="k">fun</span> <span class="nf">fromJson</span><span class="p">(</span><span class="n">reader</span><span class="p">:</span> <span class="n">JsonReader</span><span class="p">):</span> <span class="n">TextParts</span><span class="p">?</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">TODO</span><span class="p">(</span><span class="s2">&#34;Not implemented&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nd">@ToJson</span>
</span></span><span class="line"><span class="cl">  <span class="k">fun</span> <span class="nf">toJson</span><span class="p">(</span><span class="n">writer</span><span class="p">:</span> <span class="n">JsonWriter</span><span class="p">:</span> <span class="k">value</span><span class="p">:</span> <span class="n">TextParts</span><span class="p">?)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">TODO</span><span class="p">(</span><span class="s2">&#34;Not implemented&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Now we&rsquo;re ready to start parsing. First, let&rsquo;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&rsquo;m going to do it in one go and leave comments inline to explain what&rsquo;s happening.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="nd">@ToJson</span>
</span></span><span class="line"><span class="cl"><span class="k">fun</span> <span class="nf">toJson</span><span class="p">(</span><span class="n">writer</span><span class="p">:</span> <span class="n">JsonWriter</span><span class="p">:</span> <span class="k">value</span><span class="p">:</span> <span class="n">TextParts</span><span class="p">?)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// Null values shouldn&#39;t arrive to the adapter, this error lets callers know
</span></span></span><span class="line"><span class="cl">  <span class="c1">// what builder options need to be passed to the Moshi.Builder() instance
</span></span></span><span class="line"><span class="cl">  <span class="c1">// to avoid this particular situation.
</span></span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="k">value</span> <span class="o">==</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">throw</span> <span class="n">NullPointerException</span><span class="p">(</span><span class="s2">&#34;value was null! Wrap in .nullSafe() to write nullable values.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// Use the Kotlin `with` scoping method so we don&#39;t need to call
</span></span></span><span class="line"><span class="cl">  <span class="c1">// all methods with the `writer.` prefix.
</span></span></span><span class="line"><span class="cl">  <span class="n">with</span><span class="p">(</span><span class="n">writer</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Start the JSON object.
</span></span></span><span class="line"><span class="cl">    <span class="n">beginObject</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Since our `extras` field is nullable, and our backend will send
</span></span></span><span class="line"><span class="cl">    <span class="c1">// it as a literal null rather than skip it, we want null values to
</span></span></span><span class="line"><span class="cl">    <span class="c1">// be written into the final JSON.
</span></span></span><span class="line"><span class="cl">    <span class="n">serializeNulls</span> <span class="p">=</span> <span class="k">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Create a JSON field with the name &#39;heading&#39;
</span></span></span><span class="line"><span class="cl">    <span class="n">name</span><span class="p">(</span><span class="s2">&#34;heading&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Set the value of the &#39;heading&#39; field to the actual heading
</span></span></span><span class="line"><span class="cl">    <span class="k">value</span><span class="p">(</span><span class="k">value</span><span class="p">.</span><span class="n">heading</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Create the &#39;extras&#39; field
</span></span></span><span class="line"><span class="cl">    <span class="n">name</span><span class="p">(</span><span class="s2">&#34;extras&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="k">value</span><span class="p">.</span><span class="n">body</span> <span class="o">!=</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="c1">// If the body text exists, then start a new object and add a
</span></span></span><span class="line"><span class="cl">      <span class="c1">// body field
</span></span></span><span class="line"><span class="cl">      <span class="n">beginObject</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">      <span class="n">name</span><span class="p">(</span><span class="s2">&#34;body&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="k">value</span><span class="p">(</span><span class="k">value</span><span class="p">.</span><span class="n">bodyText</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">endObject</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="c1">// Otherwise we put down a literal null
</span></span></span><span class="line"><span class="cl">      <span class="n">nullValue</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// End the top-level object.
</span></span></span><span class="line"><span class="cl">    <span class="n">endObject</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><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&rsquo;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>
<p>Anyways, that&rsquo;s the object -&gt; JSON part sorted. Now let&rsquo;s try to do the reverse. Here&rsquo;s where we are as of now.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="k">fun</span> <span class="nf">fromJson</span><span class="p">(</span><span class="n">reader</span><span class="p">:</span> <span class="n">JsonReader</span><span class="p">):</span> <span class="n">TextParts</span><span class="p">?</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">TODO</span><span class="p">(</span><span class="s2">&#34;Not implemented&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Same as writing JSON, we need to start by making an object.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"> fun fromJson(reader: JsonReader): TextParts? {
</span></span><span class="line"><span class="cl"><span class="gi">+  // We&#39;ll be constructing the object at the end so these
</span></span></span><span class="line"><span class="cl"><span class="gi">+  // will store the values we read.
</span></span></span><span class="line"><span class="cl"><span class="gi">+  var heading: String? = null
</span></span></span><span class="line"><span class="cl"><span class="gi">+  var body: String? = null
</span></span></span><span class="line"><span class="cl"><span class="gi">+  with(reader) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+    beginObject()
</span></span></span><span class="line"><span class="cl"><span class="gi">+    endObject()
</span></span></span><span class="line"><span class="cl"><span class="gi">+  }
</span></span></span><span class="line"><span class="cl">   TODO(&#34;Not implemented&#34;)
</span></span><span class="line"><span class="cl"> }
</span></span></code></pre></div><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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gi">+val topLevelKeys = JsonReader.Options.of(&#34;heading&#34;, &#34;extras&#34;)
</span></span></span><span class="line"><span class="cl"><span class="gi">+val extrasKeys = JsonReader.Options.of(&#34;body&#34;)
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"> fun fromJson(reader: JsonReader): TextParts? {
</span></span><span class="line"><span class="cl">   // We&#39;ll be constructing the object at the end so these
</span></span><span class="line"><span class="cl">   // will store the values we read.
</span></span></code></pre></div><p>And we&rsquo;re set. You&rsquo;ll see the significance of the Options objects now.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl">   var body: String? = null
</span></span><span class="line"><span class="cl">   with(reader) {
</span></span><span class="line"><span class="cl">     beginObject()
</span></span><span class="line"><span class="cl"><span class="gi">+    while(hasNext()) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+      when(selectName(topLevelKeys)) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+        0 -&gt; heading = nextString() ?: throw Util.unexpectedNull(
</span></span></span><span class="line"><span class="cl"><span class="gi">+          &#34;heading&#34;,
</span></span></span><span class="line"><span class="cl"><span class="gi">+          &#34;text&#34;,
</span></span></span><span class="line"><span class="cl"><span class="gi">+          this
</span></span></span><span class="line"><span class="cl"><span class="gi">+        )
</span></span></span><span class="line"><span class="cl"><span class="gi">+      }
</span></span></span><span class="line"><span class="cl"><span class="gi">+    }
</span></span></span><span class="line"><span class="cl">     endObject()
</span></span><span class="line"><span class="cl">   }
</span></span><span class="line"><span class="cl">   TODO(&#34;Not implemented&#34;)
</span></span></code></pre></div><p><code>reader.hasNext()</code> is going to continue iterating through the document&rsquo;s tokens until it&rsquo;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&rsquo;s non-nullable in <code>TextParts</code>). The <code>Util.unexpectedNull</code> method is a little nicety that is part of Moshi&rsquo;s internals and is used by its kapt-generated adapters to provide better error messages and we&rsquo;re going to do the same.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl">         0 -&gt; heading = nextString() ?: throw Util.unexpectedNull(
</span></span><span class="line"><span class="cl">           &#34;heading&#34;,
</span></span><span class="line"><span class="cl">           &#34;text&#34;,
</span></span><span class="line"><span class="cl">           this
</span></span><span class="line"><span class="cl">         )
</span></span><span class="line"><span class="cl"><span class="gi">+        -1 -&gt; {
</span></span></span><span class="line"><span class="cl"><span class="gi">+          // Skip unknown values
</span></span></span><span class="line"><span class="cl"><span class="gi">+          skipName()
</span></span></span><span class="line"><span class="cl"><span class="gi">+          skipValue()
</span></span></span><span class="line"><span class="cl"><span class="gi">+        }
</span></span></span><span class="line"><span class="cl">       }
</span></span><span class="line"><span class="cl">     }
</span></span><span class="line"><span class="cl">     endObject()
</span></span></code></pre></div><p>When I said that <code>selectName</code> returns the index of the matched key, I didn&rsquo;t mention that it returns -1 when it comes across a key that isn&rsquo;t in the Options object. Since we don&rsquo;t care about them, we&rsquo;re going to skip both their name and value and continue right on ahead. Now, we&rsquo;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>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl">           &#34;text&#34;,
</span></span><span class="line"><span class="cl">           this
</span></span><span class="line"><span class="cl">         )
</span></span><span class="line"><span class="cl"><span class="gi">+        1 -&gt; {
</span></span></span><span class="line"><span class="cl"><span class="gi">+          // &#34;extras&#34; is nullable, so we first try to see if it is null.
</span></span></span><span class="line"><span class="cl"><span class="gi">+          // If it isn&#39;t, this will throw and we can then safely assume
</span></span></span><span class="line"><span class="cl"><span class="gi">+          // a non-null value and proceed.
</span></span></span><span class="line"><span class="cl"><span class="gi">+          try {
</span></span></span><span class="line"><span class="cl"><span class="gi">+            nextNull&lt;Any&gt;()
</span></span></span><span class="line"><span class="cl"><span class="gi">+          } catch (_: JsonDataException) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+            beginObject()
</span></span></span><span class="line"><span class="cl"><span class="gi">+            while (hasNext()) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+              when (selectName(extrasKeys)) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+                0 -&gt; body = nextString()
</span></span></span><span class="line"><span class="cl"><span class="gi">+                -1 -&gt; {
</span></span></span><span class="line"><span class="cl"><span class="gi">+                  // Skip unknown values
</span></span></span><span class="line"><span class="cl"><span class="gi">+                  skipName()
</span></span></span><span class="line"><span class="cl"><span class="gi">+                  skipValue()
</span></span></span><span class="line"><span class="cl"><span class="gi">+                }
</span></span></span><span class="line"><span class="cl"><span class="gi">+              }
</span></span></span><span class="line"><span class="cl"><span class="gi">+            }
</span></span></span><span class="line"><span class="cl"><span class="gi">+            endObject()
</span></span></span><span class="line"><span class="cl"><span class="gi">+          }
</span></span></span><span class="line"><span class="cl"><span class="gi">+        }
</span></span></span><span class="line"><span class="cl">         -1 -&gt; {
</span></span><span class="line"><span class="cl">           // Skip unknown values
</span></span><span class="line"><span class="cl">           skipName()
</span></span><span class="line"><span class="cl">           skipValue()
</span></span></code></pre></div><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&rsquo;t null.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl">     }
</span></span><span class="line"><span class="cl">     endObject()
</span></span><span class="line"><span class="cl">   }
</span></span><span class="line"><span class="cl"><span class="gd">-  TODO(&#34;Not implemented&#34;)
</span></span></span><span class="line"><span class="cl"><span class="gi">+  // Satisfy the typechecker and throw in case the JSON body
</span></span></span><span class="line"><span class="cl"><span class="gi">+  // didn&#39;t contain the &#39;heading&#39; field at all
</span></span></span><span class="line"><span class="cl"><span class="gi">+  require(heading != null) { &#34;heading must not be null&#34; }
</span></span></span><span class="line"><span class="cl"><span class="gi">+  return TextParts(heading, body)
</span></span></span><span class="line"><span class="cl"> }
</span></span></code></pre></div><p>And that&rsquo;s it! The final adapter is going to look like this</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">TextPartsJsonAdapter</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">val</span> <span class="py">topLevelKeys</span> <span class="p">=</span> <span class="nc">JsonReader</span><span class="p">.</span><span class="nc">Options</span><span class="p">.</span><span class="n">of</span><span class="p">(</span><span class="s2">&#34;heading&#34;</span><span class="p">,</span> <span class="s2">&#34;extras&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">val</span> <span class="py">extrasKeys</span> <span class="p">=</span> <span class="nc">JsonReader</span><span class="p">.</span><span class="nc">Options</span><span class="p">.</span><span class="n">of</span><span class="p">(</span><span class="s2">&#34;body&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nd">@FromJson</span>
</span></span><span class="line"><span class="cl">  <span class="k">fun</span> <span class="nf">fromJson</span><span class="p">(</span><span class="n">reader</span><span class="p">:</span> <span class="n">JsonReader</span><span class="p">):</span> <span class="n">TextParts</span><span class="p">?</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// We&#39;ll be constructing the object at the end so these
</span></span></span><span class="line"><span class="cl">    <span class="c1">// will store the values we read.
</span></span></span><span class="line"><span class="cl">    <span class="k">var</span> <span class="py">heading</span><span class="p">:</span> <span class="n">String</span><span class="p">?</span> <span class="p">=</span> <span class="k">null</span>
</span></span><span class="line"><span class="cl">    <span class="k">var</span> <span class="py">body</span><span class="p">:</span> <span class="n">String</span><span class="p">?</span> <span class="p">=</span> <span class="k">null</span>
</span></span><span class="line"><span class="cl">    <span class="n">with</span><span class="p">(</span><span class="n">reader</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">beginObject</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">      <span class="k">while</span><span class="p">(</span><span class="n">hasNext</span><span class="p">())</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">when</span><span class="p">(</span><span class="n">selectName</span><span class="p">(</span><span class="n">topLevelKeys</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="mi">0</span> <span class="o">-&gt;</span> <span class="n">heading</span> <span class="p">=</span> <span class="n">nextString</span><span class="p">()</span> <span class="o">?:</span> <span class="k">throw</span> <span class="nc">Util</span><span class="p">.</span><span class="n">unexpectedNull</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;heading&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;text&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="k">this</span>
</span></span><span class="line"><span class="cl">          <span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="mi">1</span> <span class="o">-&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="c1">// &#34;extras&#34; is nullable, so we first try to see if it is null.
</span></span></span><span class="line"><span class="cl">            <span class="c1">// If it isn&#39;t, this will throw and we can then safely assume
</span></span></span><span class="line"><span class="cl">            <span class="c1">// a non-null value and proceed.
</span></span></span><span class="line"><span class="cl">            <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">              <span class="n">nextNull</span><span class="p">&lt;</span><span class="n">Any</span><span class="p">&gt;()</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">_</span><span class="p">:</span> <span class="n">JsonDataException</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">              <span class="n">beginObject</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">              <span class="k">while</span> <span class="p">(</span><span class="n">hasNext</span><span class="p">())</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="k">when</span> <span class="p">(</span><span class="n">selectName</span><span class="p">(</span><span class="n">extrasKeys</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                  <span class="mi">0</span> <span class="o">-&gt;</span> <span class="n">body</span> <span class="p">=</span> <span class="n">nextString</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">                  <span class="k">else</span> <span class="o">-&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="c1">// Skip unknown
</span></span></span><span class="line"><span class="cl">                    <span class="n">skipName</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">                    <span class="n">skipValue</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">                  <span class="p">}</span>
</span></span><span class="line"><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="cl">              <span class="p">}</span>
</span></span><span class="line"><span class="cl">              <span class="n">endObject</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">          <span class="p">}</span>
</span></span><span class="line"><span class="cl">          <span class="p">-</span><span class="mi">1</span> <span class="o">-&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="n">skipName</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">            <span class="n">skipValue</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">          <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="n">endObject</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Satisfy the typechecker and throw in case the JSON body
</span></span></span><span class="line"><span class="cl">    <span class="c1">// didn&#39;t contain the &#39;heading&#39; field at all
</span></span></span><span class="line"><span class="cl">    <span class="n">require</span><span class="p">(</span><span class="n">heading</span> <span class="o">!=</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span> <span class="s2">&#34;heading must not be null&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">TextParts</span><span class="p">(</span><span class="n">heading</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nd">@ToJson</span>
</span></span><span class="line"><span class="cl">  <span class="k">fun</span> <span class="nf">toJson</span><span class="p">(</span><span class="n">writer</span><span class="p">:</span> <span class="n">JsonWriter</span><span class="p">:</span> <span class="k">value</span><span class="p">:</span> <span class="n">TextParts</span><span class="p">?)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Null values shouldn&#39;t arrive to the adapter, this error lets callers know
</span></span></span><span class="line"><span class="cl">    <span class="c1">// what builder options need to be passed to the Moshi.Builder() instance
</span></span></span><span class="line"><span class="cl">    <span class="c1">// to avoid this particular situation.
</span></span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="k">value</span> <span class="o">==</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">throw</span> <span class="n">NullPointerException</span><span class="p">(</span><span class="s2">&#34;value was null! Wrap in .nullSafe() to write nullable values.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Use the Kotlin `with` scoping method so we don&#39;t need to call
</span></span></span><span class="line"><span class="cl">    <span class="c1">// all methods with the `writer.` prefix.
</span></span></span><span class="line"><span class="cl">    <span class="n">with</span><span class="p">(</span><span class="n">writer</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="c1">// Start the JSON object.
</span></span></span><span class="line"><span class="cl">      <span class="n">beginObject</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="c1">// Since our `extras` field is nullable, and our backend will send
</span></span></span><span class="line"><span class="cl">      <span class="c1">// it as a literal null rather than skip it, we want null values to
</span></span></span><span class="line"><span class="cl">      <span class="c1">// be written into the final JSON.
</span></span></span><span class="line"><span class="cl">      <span class="n">serializeNulls</span> <span class="p">=</span> <span class="k">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="c1">// Create a JSON field with the name &#39;heading&#39;
</span></span></span><span class="line"><span class="cl">      <span class="n">name</span><span class="p">(</span><span class="s2">&#34;heading&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="c1">// Set the value of the &#39;heading&#39; field to the actual heading
</span></span></span><span class="line"><span class="cl">      <span class="k">value</span><span class="p">(</span><span class="k">value</span><span class="p">.</span><span class="n">heading</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="c1">// Create the &#39;extras&#39; field
</span></span></span><span class="line"><span class="cl">      <span class="n">name</span><span class="p">(</span><span class="s2">&#34;extras&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="k">value</span><span class="p">.</span><span class="n">body</span> <span class="o">!=</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// If the body text exists, then start a new object and add a body field
</span></span></span><span class="line"><span class="cl">        <span class="n">beginObject</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">name</span><span class="p">(</span><span class="s2">&#34;body&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">value</span><span class="p">(</span><span class="k">value</span><span class="p">.</span><span class="n">bodyText</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">endObject</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// Otherwise we put down a literal null
</span></span></span><span class="line"><span class="cl">        <span class="n">nullValue</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="c1">// End the top-level object.
</span></span></span><span class="line"><span class="cl">      <span class="n">endObject</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><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>
]]></content:encoded>
    </item>
    
    <item>
      <title>Android Password Store October release</title>
      <link>https://msfjarvis.dev/posts/aps-october-release/</link>
      <pubDate>Thu, 22 Oct 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/aps-october-release/</guid>
      <description>Long form release notes for the Android Password Store October release</description>
      <content:encoded><![CDATA[<p>We&rsquo;re back with yet another release! As I shared earlier this month, this is going to our last release for a while. There&rsquo;s a lot of work left to be done, and we&rsquo;re simply not big enough a team to have these larger changes be done separately from our main development. We&rsquo;ll still be doing bugfix releases if and when required, so please do file bug reports as and when you encounter issues.</p>
<h2 id="new-features">New features</h2>
<h3 id="gpg-key-selection-added-to-onboarding">GPG key selection added to onboarding</h3>
<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&rsquo;s what the key selection screen looks like:</p>
<p><img alt="GPG key selection screen from the APS October release" loading="lazy" src="/posts/aps-october-release/aps-october-release-gpg-key-selection.webp"></p>
<h3 id="allow-configuring-an-https-proxy">Allow configuring an HTTPS proxy</h3>
<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>
<ul>
<li>No SOCKS5 support</li>
<li>Relatively unhelpful error messages when proxy connection fails</li>
</ul>
<h3 id="add-option-to-automatically-sync-repository">Add option to automatically sync repository</h3>
<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&rsquo;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>
<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>
<h2 id="fixes">Fixes</h2>
<h3 id="improved-error-messaging">Improved error messaging</h3>
<p>For a large set of connection related errors, the failure message would simply be &lsquo;Invalid remote: origin&rsquo;. That is exactly as unhelpful as one might think, and now we try harder to extract the actual, more meaningful error message.</p>
<h3 id="use-gits-default-user-and-email-when-none-are-configured">Use Git&rsquo;s default user and email when none are configured</h3>
<p>We don&rsquo;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>&lt;&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>
<h3 id="improvements-around-phishing-detection-ux">Improvements around phishing detection UX</h3>
<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>
<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>
<p>Secondly, some complexity with how Android&rsquo;s Autofill APIs work resulted in the &ldquo;no I&rsquo;m not being phished, accept this new signature&rdquo; 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&rsquo;s cumbersome, so we&rsquo;ve fixed it now. Cheers to Fabian for his stellar work as always!</p>
<h3 id="conclusion">Conclusion</h3>
<p>As you can notice, this is a bit of a small release by our standards. Fabian&rsquo;s been busy with his Ph.D. (!!) and the new job he&rsquo;s starting at soon (!!), and me and Aditya have been busy with our day jobs as well. This doesn&rsquo;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>
]]></content:encoded>
    </item>
    
    <item>
      <title>Android Password Store September release</title>
      <link>https://msfjarvis.dev/posts/aps-september-release/</link>
      <pubDate>Mon, 21 Sep 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/aps-september-release/</guid>
      <description>Long form release notes for the Android Password Store September release</description>
      <content:encoded><![CDATA[<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>
<blockquote>
<p>Multiple important announcements at the end of the page, make sure to read the whole thing!</p>
</blockquote>
<h2 id="new-features">New features</h2>
<h3 id="extend-autofill-support-to-more-browsers">Extend Autofill support to more browsers</h3>
<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>
<h3 id="allow-sorting-by-recently-used">Allow sorting by recently used</h3>
<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&rsquo;ll see everything already sorted based on your old usage patterns. Neat!</p>
<h3 id="add-ability-to-view-git-commit-log">Add ability to view Git commit log</h3>
<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&rsquo;s a great feature, and I thoroughly enjoyed the entire process of its inclusion.</p>
<h3 id="ssh-key-generation-and-handling-improvements">SSH key generation and handling improvements</h3>
<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&rsquo;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>
<h3 id="fallback-authentication-for-ssh">Fallback authentication for SSH</h3>
<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&rsquo;t previously supported in APS, which would quit after the first failure. We&rsquo;ve changed that to now offer the option of entering a password if the server is configured to fall back to it.</p>
<h3 id="rewritten-and-redesigned-onboarding-flow">Rewritten and redesigned onboarding flow</h3>
<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&rsquo;s real pretty now ✨</p>
<h3 id="show-hidden-folders-now-also-shows-hidden-directories">Show hidden folders now also shows hidden directories</h3>
<p>Our old &lsquo;Show hidden folders&rsquo; 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>
<h2 id="bugfixes">Bugfixes</h2>
<h3 id="ssh-connection-problems-with-bitbucket">SSH connection problems with Bitbucket</h3>
<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>
<h3 id="symlink-support">Symlink support</h3>
<p>While still potentially finicky, we&rsquo;re now confident that this is ready to be shipped to all users without the risk of crashes.</p>
<h3 id="assorted-ux-improvements">Assorted UX improvements</h3>
<p>As always, there are a handful of Quality of Life changes to make the app more enjoyable to use:</p>
<ul>
<li>When retrying password authentication, the option to see what you&rsquo;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>
<li>Authentication modes will now be dynamically hidden and shown based on the URL&rsquo;s schema so you&rsquo;re aware of what methods you have for authentication for any given remote repository.</li>
<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>
<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>
<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>
<li>Pressing the save button is no longer necessary to save changes to authentication mode.</li>
<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>
</ul>
<p>There&rsquo;s definitely more fixes here, but we ended up rewriting, breaking and fixing so many things for this release that it&rsquo;s hard to tell what was actually broken in the previous release and what is just us fixing regressions during refactoring. We&rsquo;ve been busy :)</p>
<h2 id="important-announcements">Important announcements</h2>
<h3 id="autofill-parser-is-now-a-standalone-library">Autofill parser is now a standalone library!</h3>
<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&rsquo;s something you&rsquo;re interested in.</p>
<h3 id="rfc-for-removal-of-git-support-in-external-repos">RFC for removal of Git support in external repos</h3>
<p>Based on the issues raised in the repository and the support emails I&rsquo;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&rsquo;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>
<p>As such, we are now in the process of removing Git support from these repositories. We&rsquo;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&rsquo;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>
]]></content:encoded>
    </item>
    
    <item>
      <title>Tools of the trade: SDKMAN!</title>
      <link>https://msfjarvis.dev/posts/tools-of-the-trade-sdkman/</link>
      <pubDate>Wed, 02 Sep 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/tools-of-the-trade-sdkman/</guid>
      <description>Bringing this series back on popular demand, we&amp;rsquo;re here to talk about SDKMAN!</description>
      <content:encoded><![CDATA[<p>In the fourth post of <a href="/categories/tools-of-the-trade/">this series</a>, we&rsquo;re talking about <a href="https://sdkman.io">SDKMAN!</a>.</p>
<h2 id="what-is-sdkman">What is SDKMAN?</h2>
<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>
<h2 id="why-do-i-use-it">Why do I use it?</h2>
<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&rsquo;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&rsquo; Zulu and many more.</p>
<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>
<p>To make this side-by-side ability even more useful, SDKMAN let&rsquo;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>
<p>Since the auto env feature matches what direnv does, I just use it directly. So, rather than doing this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># .sdkmanrc</span>
</span></span><span class="line"><span class="cl"><span class="nv">java</span><span class="o">=</span>8.0.262-zulu
</span></span></code></pre></div><p>I do:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># .envrc</span>
</span></span><span class="line"><span class="cl"><span class="c1"># In bash, doing `${VARIABLE/src/dest}` replaces `src` with</span>
</span></span><span class="line"><span class="cl"><span class="c1"># `dest` in `${VARIABLE}`, but you still need to write it</span>
</span></span><span class="line"><span class="cl"><span class="c1"># back manually (hence the `export`).</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">JAVA_HOME</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">JAVA_HOME</span><span class="p">/current/8.0.262-zulu</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p>for the same end result, but a lot faster.</p>
<p>And that&rsquo;s really it! SDKMAN&rsquo;s pretty neat, but for the most part it just stays out of your way and thus there&rsquo;s not a lot to talk about it. The next post&rsquo;s gonna be much more hands-on :-)</p>
<p>This was part 4 of the <a href="/categories/tools-of-the-trade/">Tools of the trade</a> series.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Android Password Store August release</title>
      <link>https://msfjarvis.dev/posts/aps-august-release/</link>
      <pubDate>Tue, 18 Aug 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/aps-august-release/</guid>
      <description>Long form release notes for the Android Password Store August release</description>
      <content:encoded><![CDATA[<p>Continuing this new tradition, here are the detailed release notes for the <a href="https://github.com/android-password-store/Android-Password-Store/releases/tag/v1.11.0">v1.11.0</a> build of of Android Password Store that is going out right now on the Play Store and to F-Droid in the coming days. The overall focus of this release has been to improve UX and resolve bugs. Regular feature development has already resumed for next month&rsquo;s release where we&rsquo;ll be bringing <a href="https://source.android.com/security/keystore">Android Keystore</a> backed SSH key generation as well as a rewritten OpenKeychain integration for SSH connections.</p>
<h1 id="new-features">New features</h1>
<h2 id="one-url-field-to-rule-them-all">One URL field to rule them all</h2>
<p>Previously you&rsquo;d have to set the URL to your repository across multiple fields like username, server, repository name and what not. Annoying! These things make sense to us as developers, but users should not have to be dealing with all that complexity when all they want to do is enter a single URL. We&rsquo;ve received numerous bug reports over time as a result of people misunderstanding and ultimately misconfiguring things when exposed to this hellscape. Thanks to some <em>amazing</em> work from Fabian, we now have a single URL field for users to fill into.</p>
<p><img alt="Single URL field in repository information" loading="lazy" src="/posts/aps-august-release/aps-august-release-single-url-field.webp"></p>
<h2 id="custom-branch-support">Custom branch support</h2>
<p>A long-requested feature (<a href="https://msfjarvis.dev/aps/issue/298">from 2017</a>!) has been the ability to change the default branch that APS uses. It was previously hard-coded to <code>master</code>, which was an issue for people who don&rsquo;t use that term or who keep separate stores on separate branches of their repository and would like to be able to switch easily. Now you can set the branch while cloning or make the change by setting it in the git server config screen, then using the &lsquo;Hard reset to remote branch&rsquo; option in Git utils to switch to it.</p>
<h2 id="xkpasswd-generator-improvements">XkPasswd generator improvements</h2>
<p>We made a number of UI improvements in this area for the last series, and for this release the original contributor <a href="https://github.com/glowinthedark">glowinthedark</a> has returned to add the ability to append extra symbols and numbers to the password. Sometimes you&rsquo;ll see sites that require that each password have at least 1 symbol and 1 number to agree with some arbitrary logic&rsquo;s idea of a &lsquo;secure&rsquo; password, and while it can be done manually, automatic is just better :)</p>
<p><img alt="XkPasswd generator with the new symbol/number append option" loading="lazy" src="/posts/aps-august-release/aps-august-release-xkpasswd.webp"></p>
<p>To add 1 symbol and 1 number to the end of a password, input <code>sd</code> and press generate. Each instance of <code>s</code> means one symbol, and <code>d</code> means one digit. Together these can be put together in any order and in any amount to create passwords conforming to any arbitrary snake-oil check. Remember, in passwords, length is king!</p>
<h2 id="improved-subdirectory-key-support">Improved subdirectory key support</h2>
<p>In the last major release we added support for <a href="/posts/aps-july-release/#proper-support-for-per-directory-keys">per-directory keys</a>. Building upon this, we now have support for also setting the key for a subdirectory when creating it.</p>
<p><img alt="Create folder dialog but key selection checkbox" loading="lazy" src="/posts/aps-august-release/aps-august-release-subdir-key-support.webp"></p>
<p>When selected, you will be prompted to select a key from OpenKeychain that will then be written into <code>your-new-directory/.gpg-id</code> which makes it compatible with all <code>pass</code> compliant apps.</p>
<h1 id="bugfixes">Bugfixes</h1>
<h2 id="detect-missing-openkeychain-properly-instead-of-crashing">Detect missing OpenKeychain properly instead of crashing</h2>
<p>Many, many people reported being unable to edit/create passwords and the app abruptly crashing. This is pretty bad UX, and we&rsquo;ve now fixed it. Users will be prompted to install OpenKeychain and once you install and return to Password Store, the app will pick up from where you left and continue the operation. Pretty neat, even if I say so myself :)</p>
<p><img alt="OpenKeychain installation prompt" loading="lazy" src="/posts/aps-august-release/aps-august-release-missing-openkeychain.webp"></p>
<h2 id="external-storage-fixes">External storage fixes</h2>
<p>A couple of regressions resulted in cloning to external storage being completely broken. This has now been fixed alongwith a workaround for a possible freezing scenario during deletion of existing files from the selected directory. We&rsquo;ve also improved the UX around cloning to external to be more straightforward and reliable.</p>
<h2 id="creating-nested-directories">Creating nested directories</h2>
<p>Previously, attempting to create directories like <code>directory1/subdirectory</code> would fail if <code>directory1</code> didn&rsquo;t already exist. This has now been fixed.</p>
<h1 id="misc-changes">Misc changes</h1>
<h2 id="uiux-tweaks">UI/UX tweaks</h2>
<p>We&rsquo;re constantly working towards a better UI for APS, and to that end we&rsquo;ve made some more improvements in this release. The password list now has dividers between individual items, and the parent path that was previously only shown on files now also does on directories. We hope this will help reduce ambiguity in results when searching, for example when you have a <code>github.com</code> subdirectory in both <code>work</code> and <code>personal</code> categories and need to find the right one quickly.</p>
<p>A longstanding to-do has been addressed as well, where the user will now be notified after a push operation if there was nothing to be pushed. Previously this would just do nothing which wasn&rsquo;t very intuitive.</p>
<p>We&rsquo;ve completely rewritten the Git operation code to use a simpler progress UI and cleaner patterns which made a lot of these improvements possible.</p>
<h2 id="disabling-keyboard-copy-by-default">Disabling keyboard copy by default</h2>
<p>The default behaviour of automatically copying to clipboard was both a bit insecure on most devices (w.r.t. unfettered clipboard access before Q) as well as counterproductive for some use-cases. In light of these, we&rsquo;ve flipped the default for clipboard copy to off. Existing users will not have their settings changed.</p>
<h1 id="conclusion">Conclusion</h1>
<p>There are more smaller improvements peppered around. We&rsquo;re constantly making improvements and adding new features, and welcome all constructive feedback through <a href="https://gitter.im/android-password-store/public">Gitter</a> or <a href="https://github.com/android-password-store/Android-Password-Store/issues">GitHub issues</a>.</p>
<p>Lastly, Android Password Store development thrives on your donations. You can sponsor the project on <a href="https://opencollective.com/Android-Password-Store">Open Collective</a>, or me directly through GitHub Sponsors by clicking <a href="https://github.com/sponsors/msfjarvis?o=esc">here</a>. GitHub Sponsors on Tier 2 and above get expedited triage times and priority on issues. You can now also buy features, faster support with issues as well as quicker bugfixes through our <a href="https://xscode.com/msfjarvis/Android-Password-Store">xs:code</a> page.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Tools of the trade: fd</title>
      <link>https://msfjarvis.dev/posts/tools-of-the-trade-fd/</link>
      <pubDate>Tue, 18 Aug 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/tools-of-the-trade-fd/</guid>
      <description>Probably the final post of this series? Let&amp;rsquo;s talk about fd!</description>
      <content:encoded><![CDATA[<p>Continuing <a href="/categories/tools-of-the-trade/">this series</a>, let&rsquo;s talk about <a href="https://github.com/sharkdp/fd">fd</a>.</p>
<h2 id="what-is-fd">What is fd?</h2>
<p><code>fd</code> is an extremely fast replacement for the GNU coreutils&rsquo; <code>find(1)</code> tool. It&rsquo;s written in Rust, and is built for humans, arguably unlike <code>find(1)</code>.</p>
<h2 id="why-do-i-use-it">Why do I use it?</h2>
<p>Other than the obvious speed benefits, one of the most critical improvements you&rsquo;ll notice in your workflow with <code>fd</code> is the presence of good defaults. By default <code>fd</code> ignores hidden files and folders, and respects <code>.gitignore</code> and similar files. Here&rsquo;s a small comparison to show you the differences between <code>fd</code> and <code>find(1)</code>&rsquo;s default behaviors.</p>
<p>Running both <code>find</code> and <code>fd</code> on the repository for this website, then piping the results into <a href="https://del.dog">del.dog</a>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ find <span class="p">|</span> paste
</span></span><span class="line"><span class="cl">https://del.dog/raw/greconillo
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ fd <span class="p">|</span> paste
</span></span><span class="line"><span class="cl">https://del.dog/raw/thelerrell
</span></span></code></pre></div><p>If you check both those links, you&rsquo;ll observe that <code>find(1)</code> has a significantly higher number of results compared to <code>fd</code>. Looking closely, you&rsquo;ll also notice that <code>find(1)</code> has dumped the entire <code>.git</code> directory into the results as well, alongwith the <code>public</code> directory of Hugo which contains the built site. These are surely important directories, but you almost <strong>never</strong> want to search through your <code>.git</code> directory or build artifacts. <code>fd</code> shines here by excluding them automatically, while being significantly faster than <code>find(1)</code> even when they&rsquo;re both returning the exact number of results.</p>
<p>On top of these, <code>fd</code> also comes with a very rich set of options that let you do many typically complex operations within <code>fd</code> itself.</p>
<h3 id="converting-all-jpeg-files-to-png">Converting all JPEG files to PNG</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ fd -tf jpg$ -x convert <span class="o">{}</span> <span class="o">{</span>.<span class="o">}</span>.png
</span></span></code></pre></div><p>Some new things here!</p>
<ul>
<li><code>-tf</code> means we only want files. There are multiple options for this in <code>fd</code>, including directory, executable, symlink, and even UNIX pipes and sockets.</li>
<li><code>jpg$</code> is our search term, in RegEx. <code>fd</code> makes use of <a href="https://github.com/BurntSushi">BurntSushi</a>&rsquo;s excellent <a href="https://github.com/rust-lang/regex">regex</a> library for extremely quick RegEx parsing, and is able to thus support it by default. You can override this by passing <code>-g</code>/<code>--glob</code> to use glob-based matching instead. RegEx itself is too complicated, and my experience with it too limited, to actually cover it all here. All you need to know here is that the <code>$</code> at the end simply means that we want <code>jpg</code> to be the final characters of our matching term.</li>
<li><code>-x</code> is one of two exec modes provided by <code>fd</code>. <code>-x</code> runs the provided command for each term separately, in a multi-threaded fashion, so for long-running tasks you might want to reduce CPU load by restricting threads using <code>--threads &lt;num&gt;</code>.</li>
<li><code>{}</code> and <code>{.}</code> are part of <code>fd</code>&rsquo;s execution placeholders that let you manipulate search results a bit more before handing them off to external commands. <code>{}</code> is replaced with the result as-is, and <code>{.}</code> strips the file extension. There are a couple more that you can check out using <code>fd --help</code>.</li>
<li><code>convert</code> is an external command from the ImageMagick suite of tools.</li>
</ul>
<h3 id="finding-and-deleting-all-files-with-a-specific-extension">Finding and deleting all files with a specific extension</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ fd -HItf <span class="se">\\</span>.xml$ -X rm -v
</span></span></code></pre></div><p>Mostly familiar now, but with some key differences.</p>
<ul>
<li><code>-H</code> and <code>-I</code> combined are used to include <strong>h</strong>idden and <strong>i</strong>gnored files into the results.</li>
<li><code>\\.xml$</code> is a more expansive RegEx that ensures that you only delete files that match <code>a_file.xml</code> and not <code>this_is_not_an_xml</code>, by ensuring that we match on <code>.xml</code> and not just <code>xml</code>. The double backslash is an escape sequence, because <code>.</code> has a special meaning in RegEx that we do not want here.</li>
<li><code>-X</code> is the other exec mode, batch. It runs the given command by passing all results as parameters in one go. Since we want to delete files, and <code>rm</code> lets you specify an arbitrary amount of arguments, we can use this and thus only run <code>rm</code> once.</li>
</ul>
<h3 id="updating-all-git-repositories-in-a-directory">Updating all git repositories in a directory</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ fd -Htd ^.git$ --maxdepth <span class="m">1</span> -x hub -C <span class="o">{</span>//<span class="o">}</span> sync
</span></span></code></pre></div><p>Already feels like home!</p>
<ul>
<li><code>-Htd</code> together search for hidden folders.</li>
<li><code>^.git$</code> matches exactly on <code>.git</code> by mandating that <code>.git</code> be both the first (^) and last ($) characters.</li>
<li><code>--maxdepth 1</code> is a speed optimization to make <code>fd</code> only check the current directory and not traverse.</li>
<li><code>-x</code> again runs each command separately</li>
<li><code>{//}</code> gives us the parent directory. For <code>msfjarvis.dev/.git</code>, this will give you <code>msfjarvis.dev</code>.</li>
</ul>
<p><a href="https://hub.github.com">hub</a> is a <code>git</code> wrapper that provides some handy features on top like <code>sync</code> which updates all locally checked out branches from their upstream remotes. You can re-implement this with some leg work but I&rsquo;ll leave that as an exercise for you.</p>
<p>And that&rsquo;s about it! Let me know what you think of <code>fd</code> and if you&rsquo;re switching to it.</p>
<p>This was part 3 of the <a href="/categories/tools-of-the-trade/">Tools of the trade</a> series.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Tools of the trade: fzf</title>
      <link>https://msfjarvis.dev/posts/tools-of-the-trade-fzf/</link>
      <pubDate>Mon, 10 Aug 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/tools-of-the-trade-fzf/</guid>
      <description>Continuing this series, let&amp;rsquo;s talk about fzf.</description>
      <content:encoded><![CDATA[<p>In the second post of <a href="/categories/tools-of-the-trade/">this series</a>, let&rsquo;s talk about <a href="https://github.com/junegunn/fzf">fzf</a>.</p>
<h2 id="what-is-fzf">What is fzf?</h2>
<p>In its simplest form, <code>fzf</code> is a <strong>f</strong>u<strong>zz</strong>y <strong>f</strong>inder. It lets you search through files, folders, any line-based text using a simple fuzzy and/or regex backed system.</p>
<p>On-demand, <code>fzf</code> can also be super fancy.</p>
<h2 id="why-do-i-use-it">Why do I use it?</h2>
<p>Because <code>fzf</code> is a search tool, you can use it to find files and folders. My most common use-case for it is a simple bash function that goes like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># find-and-open, gedit? Sorry I&#39;ll just stop.</span>
</span></span><span class="line"><span class="cl"><span class="k">function</span> fao<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> ARG<span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="nv">ARG</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">1</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[</span> -z <span class="s2">&#34;</span><span class="si">${</span><span class="nv">ARG</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    nano <span class="s2">&#34;</span><span class="k">$(</span>fzf<span class="k">)</span><span class="s2">&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    nano <span class="s2">&#34;</span><span class="k">$(</span>fzf -q<span class="s2">&#34;</span><span class="si">${</span><span class="nv">ARG</span><span class="si">}</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><p>It starts up a fzf session and then opens up the selected file in <code>nano</code>.</p>
<center><div style="margin-top: 2em; margin-bottom: 2em;"><script src="https://asciinema.org/a/gCwYg97C1NbRVgCUK0Dd1byVl.js" id="asciicast-gCwYg97C1NbRVgCUK0Dd1byVl" async></script></div></center>

<p>By default, <code>fzf</code> is a full-screen tool and takes up the entire height of your terminal. I&rsquo;ve restricted it to 40% of that, as it looks a bit nicer IMO. You can make more such changes by setting the <code>FZF_DEFAULT_OPTS</code> environment variable as described in the <a href="https://github.com/junegunn/fzf#layout">layout section</a> of the fzf docs.</p>
<p>But that&rsquo;s not all! You can get <em>real</em> fancy with <code>fzf</code>.</p>
<p>For example, check out the output of <code>fzf --preview 'bat --style=numbers --color=always --line-range :500 {}'</code> <a href="https://asciinema.org/a/WFFx2negPw5iXbCZe1YlAZeqj">here</a> (a bit too wide to embed here :()</p>
<blockquote>
<p><code>bat</code> is a <code>cat(1)</code> clone with syntax highlighting and other nifty features, and also a tool I use on the daily. We&rsquo;ll probably be covering it soon :)</p>
</blockquote>
<p>You can also bind arbitrary keys to actions with relative ease.</p>
<center><div style="margin-top: 2em; margin-bottom: 2em;"><script src="https://asciinema.org/a/l7OPG4xQv5QVtvyxQfmly2eiE.js" id="asciicast-l7OPG4xQv5QVtvyxQfmly2eiE" async></script></div></center>

<p>The syntax as evident, is pretty simple</p>
<pre tabindex="0"><code>&lt;key-shortcut&gt;:execute(&lt;command&gt;)&lt;+abort&gt;
</code></pre><p>The <code>+abort</code> there is optional, and signals <code>fzf</code> that we want to exit after running the command. Detailed instructions are available in the <code>fzf</code> <a href="https://github.com/junegunn/fzf#readme">README</a>.</p>
<p>And that&rsquo;s it from me. Post any fancy <code>fzf</code> recipes you come up with in the comments below!</p>
<p>This was part 2 of the <a href="/categories/tools-of-the-trade/">Tools of the trade</a> series.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Tools of the trade: direnv</title>
      <link>https://msfjarvis.dev/posts/tools-of-the-trade-direnv/</link>
      <pubDate>Tue, 04 Aug 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/tools-of-the-trade-direnv/</guid>
      <description>In the first post in the new &amp;lsquo;Tools of the trade&amp;rsquo; series, we talk about direnv.</description>
      <content:encoded><![CDATA[<p>This post was supposed to be a monolith directory of all the CLI-based tooling that I use to get things done throughout my day, but it turned out to be just a bit too long so I elected to split it out into separate posts.</p>
<p>Let&rsquo;s talk about <a href="https://github.com/direnv/direnv">direnv</a>.</p>
<h2 id="what-is-direnv">What is direnv?</h2>
<p>On the face of it, it&rsquo;s not very interesting. Their GitHub description simply reads &lsquo;Unclutter your .profile&rsquo;, which gives you a general idea of what to expect but also grossly undersells it.</p>
<p>What direnv does, is improve the experience with things like <a href="https://en.wikipedia.org/wiki/Twelve-Factor_App_methodology">12 factor apps</a>. It enables per-directory configurations that would otherwise be &lsquo;global&rsquo;. Let&rsquo;s look into how I use it, to get a robust idea of what you can expect.</p>
<h2 id="why-do-i-use-it">Why do I use it?</h2>
<p>I have a separate account for proprietary work related things <a href="https://github.com/hshandilya-navana">here</a>, which means that any GitHub tooling I use now needs to be configured with separate credentials for when I&rsquo;m interacting with work repositories. Bummer!</p>
<p><code>direnv</code> makes this simpler by allowing for environment variables to be set for those repositories only. I mostly use the official GitHub CLI from <a href="https://github.com/cli/cli">here</a> to interact with the remote repo, so providing a separate GitHub token is just a matter of setting the <code>GITHUB_TOKEN</code> environment variable to one that is allowed to interact with the current repo. With direnv, all you need to do is create a <code>.envrc</code> file in the repository directory with this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">GITHUB_TOKEN</span><span class="o">=</span>&lt;redacted&gt;
</span></span></code></pre></div><p>and <code>direnv</code> will automatically set it when you enter the directory, and more importantly: <strong>reset</strong> it back to its previous value when you exit. This &lsquo;unloading&rsquo; feature makes <code>direnv</code> extremely powerful.</p>
<center><div style="margin-top: 2em; margin-bottom: 2em;"><script src="https://asciinema.org/a/qMkuyVjPSkhNqO6Jo0eQnLiyt.js" id="asciicast-qMkuyVjPSkhNqO6Jo0eQnLiyt" async></script></div></center>

<p><code>direnv</code> also comes with a rich stdlib that lets you do far more than just export environment variables.</p>
<p>Setting up a Python virtualenv:</p>
<center><div style="margin-top: 2em; margin-bottom: 2em;"><script src="https://asciinema.org/a/irkZWRh00gFVIcH41BRcOvowm.js" id="asciicast-irkZWRh00gFVIcH41BRcOvowm" async></script></div></center>

<p>Stripping entries from <code>$PATH</code>:</p>
<center><div style="margin-top: 2em; margin-bottom: 2em;"><script src="https://asciinema.org/a/vbzolwrYnXzBFvhAqMJEFBNRv.js" id="asciicast-vbzolwrYnXzBFvhAqMJEFBNRv" async></script></div></center>

<p>Adding entries into <code>$PATH</code>:</p>
<center><div style="margin-top: 2em; margin-bottom: 2em;"><script src="https://asciinema.org/a/C1EhhAoy1y3vSwJaIc0R8o0RY.js" id="asciicast-C1EhhAoy1y3vSwJaIc0R8o0RY" async></script></div></center>

<blockquote>
<p>You&rsquo;ll notice an unfamiliar <code>rg -c</code> command there, it&rsquo;s <a href="https://github.com/BurntSushi/ripgrep">ripgrep</a>, and the <code>-c</code> flag counts the number of matches in the string if there are any, and nothing otherwise. We&rsquo;ll talk about it later in this series :)</p>
</blockquote>
<p>The possibilities are huge! To check out the stdlib yourself, run <code>direnv stdlib</code> after installing <code>direnv</code>.</p>
<p>This was part 1 of the <a href="/categories/tools-of-the-trade/">Tools of the trade</a> series.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Android Password Store 1.10.2 patch release</title>
      <link>https://msfjarvis.dev/posts/aps-1.10.2-release/</link>
      <pubDate>Thu, 30 Jul 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/aps-1.10.2-release/</guid>
      <description>Long form release notes for the Android Password Store v1.10.2 patch release</description>
      <content:encoded><![CDATA[<p>Exactly one week after the <a href="/posts/aps-1.10.1-release">previous patch release</a>, we have another small release fixing a few bugs that were deemed too high-priority for our usual release cadence.</p>
<p>List of the patches included in this release:</p>
<ul>
<li><a href="https://github.com/android-password-store/Android-Password-Store/pull/985">#985</a> fixes a couple of crashes originating in the new SMS OTP autofill feature that completely broke it.</li>
<li><a href="https://github.com/android-password-store/Android-Password-Store/pull/982">#982</a> ensures that the &lsquo;Add TOTP&rsquo; button only shows when its needed to.</li>
<li><a href="https://github.com/android-password-store/Android-Password-Store/pull/969">#969</a> improves support for pass entries that only contain TOTP URIs, and no password.</li>
</ul>
<p>This release has been uploaded to the Play Store and should reach users in a few hours. F-Droid is <a href="https://gitlab.com/fdroid/fdroiddata/-/merge_requests/7141">yet to merge</a> our MR to support the free flavor we&rsquo;ve created for them so just like the previous two release in the 1.10.x generation, this too shall not be available on their store just yet.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Android Password Store 1.10.1 patch release</title>
      <link>https://msfjarvis.dev/posts/aps-1.10.1-release/</link>
      <pubDate>Thu, 23 Jul 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/aps-1.10.1-release/</guid>
      <description>Long form release notes for the Android Password Store v1.10.1 patch release</description>
      <content:encoded><![CDATA[<p>Hot on the heels of the <a href="https://github.com/android-password-store/Android-Password-Store/releases/tag/v1.10.0">v1.10.0</a> release we have an incremental bugfix update ready to go!</p>
<p>As mentioned in the <a href="/posts/aps-july-release">previous release notes</a>, the algorithm for handling GPG keys was significantly overhauled and thus had the potential to cause some breakage. Well, it did.</p>
<p>This release includes 3 separate fixes for different bugs around GPG.</p>
<ul>
<li><a href="https://msfjarvis.dev/aps/pr/959">#959</a> ensures long key IDs are correctly parsed as hex numbers.</li>
<li><a href="https://msfjarvis.dev/aps/pr/960">#960</a> fixes a type problem where we incorrectly used a <code>Array&lt;Long&gt;</code> that gets interpreted as a <code>Serializable</code> as opposed to the <code>Long[]</code> expected by OpenKeychain.</li>
<li><a href="https://msfjarvis.dev/aps/pr/958">#958</a> reintroduces the key selection flow, adding it as a fallback for when no key has been entered into the <code>.gpg-id</code> file. This notably helps users who generate stores within the app.</li>
</ul>
<p>The release is going up on the <a href="https://play.google.com/store/apps/details?id=dev.msfjarvis.aps">Play Store</a> over the next few hours, <a href="https://f-droid.org/packages/dev.msfjarvis.aps/">F-Droid</a> builds will be delayed until our patch <a href="https://gitlab.com/fdroid/fdroiddata/-/merge_requests/7141">shifting F-Droid to the free flavor</a> is not merged.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Why upgrade Android?</title>
      <link>https://msfjarvis.dev/posts/why-upgrade-android/</link>
      <pubDate>Thu, 23 Jul 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/why-upgrade-android/</guid>
      <description>(Mostly) everybody agrees that Android upgrades are good, but how very crucial they are to security and privacy often gets overlooked. Let&amp;rsquo;s dig into that.</description>
      <content:encoded><![CDATA[<p>A couple days ago I came across a security conscious user who was quick to point out why a particular feature had to be added to APS, but failed to realise the fact that the problem wouldn&rsquo;t even exist if they were running the latest version of Android (we&rsquo;ll talk about the behavior change that fixed it later here).</p>
<p>Android upgrades bring massive changes to the platform, improving security against both known and unknown threats. You sign off that benefit when you buy into an incompetent OEM&rsquo;s cheap phones, and it has become a bit too &rsquo;normal&rsquo; than anybody would prefer.</p>
<p>That&rsquo;s not what we&rsquo;re going to talk about, though. This post is going to be purely about privacy, and how it has changed, nay, improved, over the years of Android. My apps support a minimum of Android 6, so I will begin with the next version, Android 7, and go through Google&rsquo;s release notes, singling out privacy related changes.</p>
<h2 id="android-7">Android 7</h2>
<p>Android 7 had a very passing focus on privacy and thus did not have a lot of obvious or concrete changes around it. Background execution limits introduced in Android 6 were improved in Android 7 to apply even more restrictions after devices became stationary, which can be loosely interpreted as &lsquo;bad&rsquo; for data exfiltration SDKs that apps ship but in reality didn&rsquo;t do much.</p>
<h2 id="android-8">Android 8</h2>
<h3 id="locking-down-background-location-access">Locking down background location access</h3>
<p>In Android 8, access to background location was <a href="https://developer.android.com/about/versions/oreo/android-8.0-changes#abll">severely throttled</a>. Apps received less frequent updates for location and thus couldn&rsquo;t track you in real time.</p>
<h3 id="introduction-of-autofill">Introduction of Autofill</h3>
<p>The Android Autofill framework was debuted, along with support for <a href="https://developer.android.com/about/versions/oreo/android-8.0-changes#wfa">Web form Autofill</a>. This paved the way for password managers to fill fields for you without relying on hacked up accessibility services or the Android clipboard. This was a major win!</p>
<h3 id="better-https-defaults">Better HTTPS defaults</h3>
<p>Android 8.0&rsquo;s implementation of HttpsURLConnection did not perform <a href="https://developer.android.com/about/versions/oreo/android-8.0-changes#networking-all">insecure TLS/SSL protocol version fallback</a>, which means connections that failed to negotiate a requested TLS version would now abort rather than fall back to an older version of TLS.</p>
<h3 id="android_id-changes">ANDROID_ID changes</h3>
<p>Access to the <code>ANDROID_ID</code> field <a href="https://developer.android.com/about/versions/oreo/android-8.0-changes#privacy-all">was changed significantly</a>. It is generated per-app and per-signature as opposed to the entire system making it harder to fingerprint users who have multiple apps installed with the same advertising-related SDKs.</p>
<h2 id="android-9">Android 9</h2>
<h3 id="limited-access-to-sensors">Limited access to sensors</h3>
<p>Beginning Android 9, <a href="https://developer.android.com/about/versions/pie/android-9.0-changes-all#bg-sensor-access">background access to device sensors</a> was greatly reduced. Access to microphone and camera was completely denied, and so was the gyroscope, accelerometer and other sensors of that class.</p>
<h3 id="granular-call-log-access">Granular call log access</h3>
<p>For apps that need to access the user&rsquo;s call logs for any reason, a <a href="https://developer.android.com/about/versions/pie/android-9.0-changes-all#restrict-access-call-logs">new permission group was introduced</a>. Now, you don&rsquo;t require granting access to all phone-related permissions to let an app back up your call logs.</p>
<h3 id="restricted-access-to-phone-numbers">Restricted access to phone numbers</h3>
<p>There are multiple ways to monitor phone calls on Android, and with the introduction of the <code>CALL_LOG</code> permission group, <a href="https://developer.android.com/about/versions/pie/android-9.0-changes-all#restrict-access-phone-numbers">these were locked down</a> to only expose phone numbers to apps that were allowed explicit access to call logs.</p>
<h3 id="making-wi-fi-and-cellular-networks-less-privacy-invasive">Making Wi-Fi and cellular networks less privacy invasive</h3>
<p>A combination of changes to <a href="https://developer.android.com/about/versions/pie/android-9.0-changes-all#restricted_access_to_wi-fi_location_and_connection_information">what permissions apps require</a> to know about your WiFi and <a href="https://developer.android.com/about/versions/pie/android-9.0-changes-all#information_removed_from_wi-fi_service_methods">how much personally identifiable data is provided by these APIs</a> further improves your privacy against rogue apps. Disabling device location <a href="https://developer.android.com/about/versions/pie/android-9.0-changes-all#telephony_information_now_relies_on_device_location_setting">now disables the ability to get information on cell towers</a> your phone is connected to.</p>
<h3 id="no-more-serials">No more serials</h3>
<p>Requesting access to the device serial number <a href="https://developer.android.com/about/versions/pie/android-9.0-changes-28#build-serial-deprecation">now requires phone state permissions</a> making it more explicit when apps are trying to fingerprint you.</p>
<h2 id="android-10">Android 10</h2>
<h3 id="scoped-storage">Scoped Storage</h3>
<p>Probably the most controversial change in 10, Scoped Storage segregated the device storage into scopes and <a href="https://developer.android.com/about/versions/10/privacy/changes#scoped-storage">gave apps access to them without needing extra permissions</a>.</p>
<h3 id="explicit-background-permission-access">Explicit background permission access</h3>
<p>Android 10 introduces the <code>ACCESS_BACKGROUND_LOCATION</code> permission and <a href="https://developer.android.com/about/versions/10/privacy/changes#app-access-device-location">completely disables background access</a> for apps targeting SDK 29 that don&rsquo;t declare it. For older apps, the framework treats granting location access as effectively background location access. When the app upgrades to target SDK 29, the background permission is revoked and must be explicitly requested again.</p>
<h3 id="removal-of-contacts-affinity">Removal of contacts affinity</h3>
<p>Beginning Android 10, the system no longer <a href="https://developer.android.com/about/versions/10/privacy/changes#contacts-affinity">keeps track of what contacts you interact with most</a> and thus search results are not weighted anymore.</p>
<h3 id="mac-randomization-enabled-by-default">MAC randomization enabled by default</h3>
<p>Connecting to a Wi-FI network now uses a <a href="https://developer.android.com/about/versions/10/privacy/changes#randomized-mac-addresses">randomized MAC address</a> to prevent fingerprinting.</p>
<h3 id="removal-of-access-to-non-resettable-identifiers">Removal of access to non-resettable identifiers</h3>
<p>Access to identifiers such as IMEI and serial was <a href="https://developer.android.com/about/versions/10/privacy/changes#non-resettable-device-ids">restricted to privileged apps</a> which means apps served by the Play Store can no longer see them.</p>
<h3 id="restriction-on-clipboard-access">Restriction on clipboard access</h3>
<p>This is the problem we first talked about. Apps before Android 10 could monitor clipboard events and potentially exfil confidential data like passwords. <a href="https://developer.android.com/about/versions/10/privacy/changes#clipboard-data">In Android 10 this was completely disabled</a> for apps that were not in foreground or not your active input method. This change was made with <strong>no</strong> compatibility changes, which means even older apps would not be able to access clipboard data out of turn.</p>
<h3 id="more-wifi-and-location-improvements">More WiFi and location improvements</h3>
<p>Apps can no longer <a href="https://developer.android.com/about/versions/10/privacy/changes#enable-disable-wifi">toggle WiFi</a> or <a href="https://developer.android.com/about/versions/10/privacy/changes#configure-wifi">read a list of configured networks</a>, and getting access to methods that expose device location requires the <code>ACCESS_FINE_LOCATION</code> permission to make it obvious that an app is doing it. The last change also affects telephony related APIs, a full list is available <a href="https://developer.android.com/about/versions/10/privacy/changes#telephony-apis">here</a>.</p>
<h3 id="permissions-controls">Permissions controls</h3>
<p>Apps no longer have <a href="https://developer.android.com/about/versions/10/privacy/changes#screen-contents">silent access to screen contents</a>, and the platform now prompts users <a href="https://developer.android.com/about/versions/10/privacy/changes#user-permission-legacy-apps">to disallow permissions for legacy apps</a> that target Android 5.1 or below that would earlier be granted at install time. <a href="https://developer.android.com/about/versions/10/privacy/changes#physical-activity-recognition">Physical activity recognition</a> is now given its own permission and common libraries for the purpose like Google&rsquo;s Play Services APIs will now send empty data when an app requests activity without the permissions.</p>
<h2 id="android-11-tentative">Android 11 (tentative)</h2>
<h3 id="storage-changes">Storage changes</h3>
<ul>
<li>
<p>Apps targeting Android 11 are <a href="https://developer.android.com/about/versions/11/privacy/storage#scoped-storage">no longer allowed to opt out of scoped storage</a>.</p>
</li>
<li>
<p>All encompassing access to a large set of directories and files is <a href="https://developer.android.com/about/versions/11/privacy/storage#file-directory-restrictions">completely disabled</a>, including the root of the internal storage, the <code>Download</code> folder, and the data and obb subdirectories of the <code>Android</code> folder.</p>
</li>
</ul>
<h3 id="permission-changes">Permission changes</h3>
<ul>
<li>
<p>Location, microphone and camera related permissions can now <a href="https://developer.android.com/about/versions/11/privacy/permissions#one-time">be granted on a one-off basis</a>, meaning they&rsquo;ll automatically get revoked when the app process exits.</p>
</li>
<li>
<p>Apps that are not used for a few months will <a href="https://developer.android.com/about/versions/11/privacy/permissions#auto-reset">have their permissions automatically revoked</a>.</p>
</li>
<li>
<p>A new <code>READ_PHONE_NUMBERS</code> permission <a href="https://developer.android.com/about/versions/11/privacy/permissions#phone-numbers">has been added</a> to call certain APIs that expose phone numbers.</p>
</li>
</ul>
<h3 id="location-changes">Location changes</h3>
<ul>
<li>
<p><a href="https://developer.android.com/about/versions/11/privacy/location#one-time-access">One time access</a> is now an option for location, allowing users to not grant persistent access when they don&rsquo;t wish to.</p>
</li>
<li>
<p>Background location needs to <a href="https://developer.android.com/about/versions/11/privacy/location#background-location">be requested separately now</a> and asking for it together with foreground location will throw an exception.</p>
</li>
</ul>
<h3 id="data-access-auditing">Data access auditing</h3>
<p>To allow apps to audit their own usage of user data, <a href="https://developer.android.com/about/versions/11/privacy/data-access-auditing#log-access">a new callback is provided</a>. Apps can implement it and then log all accesses to see if there&rsquo;s any unexpected data use that needs to be resolved.</p>
<h3 id="redacted-mac-addresses">Redacted MAC addresses</h3>
<p>Unpriviledged apps targeting SDK 30 will no longer be able to get the device&rsquo;s real MAC address.</p>
<h1 id="closing-notes">Closing notes</h1>
<p>As you can tell, improving user privacy is a constant journey and Android is doing a better job of it with every new release. This makes it crucial that you stay up-to-date, either by buying phones from an OEM that delivers timely updates for a sufficiently long support period, or by using a trusted custom ROM like <a href="https://grapheneos.org/">GrapheneOS</a> or <a href="https://lineageos.org/">LineageOS</a>.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Android Password Store July release</title>
      <link>https://msfjarvis.dev/posts/aps-july-release/</link>
      <pubDate>Wed, 22 Jul 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/aps-july-release/</guid>
      <description>Long form release notes for the Android Password Store July release</description>
      <content:encoded><![CDATA[<p>As promised, here are detailed release notes for the <a href="https://github.com/android-password-store/Android-Password-Store/releases/tag/v1.10.0">v1.10.0</a> build of Android Password Store that is going out right now on the Play Store and to F-Droid in the coming days. This is a massive one even compared to our previous v1.9.0 major release, which was our largest release when it went out. Let&rsquo;s dive into the changes!</p>
<h2 id="new-features">New features</h2>
<h3 id="totp-support">TOTP support</h3>
<p>I <a href="https://msfjarvis.dev/aps/pr/806">removed support for HOTP and TOTP secrets</a> back in v1.9.0 due to multiple reasons, a) it was blocking important refactoring efforts, b) it had zero test coverage, and c) none of the maintainers used it. Play Store reviews swiftly reminded us that people did use the feature even in its wonky state, and demanded its return. I stuck to our decision as maintainers for a while, but active members of the pass community like <a href="https://github.com/erayd">erayd</a> (who happens to be the maintainer for <a href="https://github.com/browserpass">browserpass</a>!) were able to convince us otherwise and provided good, actionable feedback allowing us to <a href="https://msfjarvis.dev/aps/pr/890">bring back TOTP</a> support into APS, better than ever before.</p>
<p>The new implementation is backed by a solid suite of tests and contains new features like the ability to import TOTP URIs using QR codes, being able to Autofill them into webpages as well as extracting OTPs from SMSes (not available on F-Droid due to GMS dependencies for SMS monitoring).</p>
<h3 id="support-for-ed25519ecdsa-keys">Support for ED25519/ECDSA keys</h3>
<p>With our ongoing efforts to switch over from the dated <a href="http://www.jcraft.com/jsch/">Jsch</a> SSH library to the more up-to-date and maintained <a href="https://github.com/hierynomus/sshj">SSHJ</a>, we now fully support ED25519 and ECDSA keys! You no longer need to rely on RSA to authenticate from your phone to your Git host :)</p>
<p>In a future release, we&rsquo;ll be bringing more improvements to this area including generating and storing SSH keys in the <a href="https://source.android.com/security/keystore/">Android Keystore</a> for enhanced security as well as support for fallback authentication.</p>
<h3 id="proper-support-for-per-directory-keys">Proper support for per-directory keys</h3>
<p><a href="https://www.passwordstore.org/">pass</a> has a neat feature where it allows you to use a separate GPG key for a subdirectory, such as for sharing passwords across a team. It achieves this by looking for a <code>.gpg-id</code> file starting from the current directory, up to the root of the store. The first file it finds is what it uses as the key for the GPG operations.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ tree -a store
</span></span><span class="line"><span class="cl">store
</span></span><span class="line"><span class="cl">├── .gpg-id &lt;-- contains the key ABCDE01234
</span></span><span class="line"><span class="cl">└── subdirectory1
</span></span><span class="line"><span class="cl">    └── .gpg-id &lt;-- contains the key FGHIJ56789
</span></span></code></pre></div><p>In this directory structure, <code>pass generate subdirectory1/example.com</code> will use the <code>FGHIJ56789</code> key, and <code>pass generate example.com</code> will use <code>ABCDE12345</code>.</p>
<p>Previously, Password Store would only correctly handle decryption in this situation, and fail to select the right key for encrypting. The workaround for this was to manually select the key from settings that you wished to use, before creating a password. That&rsquo;s pretty stupid, and we&rsquo;re sorry you had to do that earlier. Now, Password Store uses an algorithm similar to the <code>pass</code> CLI to find the correct <code>.gpg-id</code> file and read the key from it. GnuPG is more &lsquo;forgiving&rsquo;, if you will, in what type of key values it can work with so there&rsquo;s a slim chance that your current workflow might now be broken. If this happens, please immediately either file an issue over on the <a href="https://msfjarvis.dev/aps">GitHub repository</a> or email us at <a href="mailto:aps@msfjarvis.dev">aps@msfjarvis.dev</a> with as much detail as you can and we&rsquo;ll resolve it ASAP.</p>
<h2 id="bugfixes">Bugfixes</h2>
<h3 id="better-protection-against-invalid-filename-changes">Better protection against invalid filename changes</h3>
<p>Over the past few releases we&rsquo;ve been hard at work improving the password edit flow, making it more accessible and &lsquo;obvious&rsquo; to users and simultaneously prevent any hidden footguns from souring the experience. We received a bug report about <a href="https://msfjarvis.dev/aps/issue/928">file renaming</a> having unexpected behavior that caused destructive actions in the store, and in response we <a href="https://msfjarvis.dev/aps/pr/929">now have better safeguards against this</a> and have improved the UI to make things more clear to users.</p>
<h3 id="export-passwords-asynchronously">Export passwords asynchronously</h3>
<p>Previously the password export would run on the main thread and potentially cause the app to completely freeze and throw a &lsquo;Password Store is not responding error&rsquo;. This has been rectified, and the export now occurs in an entirely separate process.</p>
<h3 id="ui-fixes">UI fixes</h3>
<p>A bunch of UI feedback was provided to us after the last major release and we&rsquo;ve worked to address it in this one. Long file/folder names now correctly wrap across lines, and the error UI for wrong password/passphrase is now aesthetically correct [<a href="https://msfjarvis.dev/aps/pr/892">PR</a>].</p>
<h3 id="qol-improvements">QoL improvements</h3>
<p>We&rsquo;ve been aggressively refactoring the codebase to use modern APIs like <a href="https://msfjarvis.dev/aps/pr/910">ActivityResultContracts</a> and making large scale architectural changes to our old code in efforts to improve maintainability in the future. We also have work-in-progress rewrites of the <a href="https://msfjarvis.dev/aps/pr/865">Git commands pipeline</a> and incoming support for <a href="https://msfjarvis.dev/aps/pr/825">fallback authentication</a>.</p>
<h2 id="general-changes-and-improvements">General changes and improvements</h2>
<h3 id="new-icon-and-color-scheme">New icon and color scheme</h3>
<p>Right off the bat, you will notice a brand new icon for Password Store. This was created for us by <a href="https://twitter.com/RKBDI">Radek Błędowski</a>, go check him out!</p>
<p><img alt="New icon" loading="lazy" src="/posts/aps-july-release/aps_banner.webp"></p>
<p>To complement the new icon, we&rsquo;ve also updated our color scheme to better suit this new branding.</p>
<h3 id="simplified-xkpasswd-implementation">Simplified XkPasswd implementation</h3>
<p>While revisiting our UI during the icon change, we realised that the alternate XkPasswd password generator option we introduced back in v1.6.0 was a tad too complicated to use with a lot more knobs and switches than necessary. This has been fixed, and we hope that it&rsquo;s now at a level of accessibility that allows more users to try it out.</p>
<h3 id="improvements-to-biometric-lock-transition-and-password-list-ui">Improvements to biometric lock transition and password list UI</h3>
<p>The biometric authentication UI flow has been updated to show the authentication dialog over a transparent screen, before starting the app upon success. We&rsquo;ve also retouched the password list to remove the leading icons, as we have been consistently receiving numerous comments about them being unnecessary and a bit ugly. In v1.4.0 we introduced child counts and iconographic hints to directories, and we feel they are more than sufficient to communicate the difference between them and password files. We welcome all feedback about these changes at <a href="mailto:me@msfjarvis.dev">me@msfjarvis.dev</a>.</p>
<h2 id="in-conclusion">In conclusion</h2>
<p>There are a lot more changes in this release than those included in this post, which you can check out <a href="https://github.com/android-password-store/Android-Password-Store/milestone/10">here</a>. We&rsquo;re constantly at work improving APS and all constructive feedback helps us create a better experience for users and ourselves, so please keep it coming (over email, if it&rsquo;s a suggestion. Play Store reviews are not good for back-and-forth communication).</p>
<p>Lastly, Android Password Store development thrives on your donations. You can sponsor the project on <a href="https://opencollective.com/Android-Password-Store">Open Collective</a>, or me directly through GitHub Sponsors by clicking <a href="https://github.com/sponsors/msfjarvis?o=esc">here</a>. GitHub Sponsors on Tier 2 and above get expedited triage times and priority on issues :)</p>
<p>See you next month!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Making a Bluetooth adapter work on Linux</title>
      <link>https://msfjarvis.dev/posts/making-a-bluetooth-adapter-work-on-linux/</link>
      <pubDate>Fri, 17 Jul 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/making-a-bluetooth-adapter-work-on-linux/</guid>
      <description>Getting a USB Bluetooth dongle to function properly on Linux proved to be somewhat of a trip, which I&amp;rsquo;m documenting here.</description>
      <content:encoded><![CDATA[<p>I made a couple of purchases yesterday, including a Bluetooth speaker and a USB Bluetooth dongle to pair it to my computer. Now here&rsquo;s a couple things that you need to know about said computer:</p>
<ul>
<li>It runs Linux</li>
<li>It runs a customized build of the Zen kernel with a very slimmed down config</li>
<li>It has never had Bluetooth connectivity before</li>
</ul>
<p>Thanks to this combination of factors, things got weird. I tried a bunch of things before getting it working, so it is entirely possible that I miss some steps that were important but I didn&rsquo;t think so while writing this. Let me know in the comments if I missed something.</p>
<h3 id="getting-the-right-packages">Getting the right packages</h3>
<p>You&rsquo;re gonna need 1) a GUI to handle BT devices, b) the PulseAudio module for Bluetooth. For the GUI I used <a href="http://packages.linuxmint.com/search.php?release=ulyana&amp;section=main&amp;keyword=blueberry">blueberry</a>, and <a href="https://packages.ubuntu.com/focal/pulseaudio-module-bluetooth">pulseaudio-module-bluetooth</a> for PulseAudio support.</p>
<p>I did <code>apt install -y blueberry pulseaudio-module-bluetooth</code> to get these on Linux Mint, you should use whatever your distro&rsquo;s preferred package management interface is.</p>
<h3 id="fixing-up-the-kernel-optional">Fixing up the kernel (optional)</h3>
<p>I mentioned earlier that I run a very slimmed down config, which means nothing that I didn&rsquo;t already use was enabled. This included Bluetooth, so I went ahead and enabled all the configs for it <a href="https://msfjarvis.dev/g/linux/992c2d8bce8b">here</a>, then installed the new kernel and rebooted into it. You shouldn&rsquo;t need to do this if you do not run a custom kernel. To be completely sure, check your dmesg for Bluetooth initialization logs:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ dmesg <span class="p">|</span> rg Bluetooth
</span></span><span class="line"><span class="cl"><span class="o">[</span>    0.146115<span class="o">]</span> Bluetooth: Core ver 2.22
</span></span><span class="line"><span class="cl"><span class="o">[</span>    0.146118<span class="o">]</span> Bluetooth: HCI device and connection manager initialized
</span></span><span class="line"><span class="cl"><span class="o">[</span>    0.146119<span class="o">]</span> Bluetooth: HCI socket layer initialized
</span></span><span class="line"><span class="cl"><span class="o">[</span>    0.146119<span class="o">]</span> Bluetooth: L2CAP socket layer initialized
</span></span><span class="line"><span class="cl"><span class="o">[</span>    0.146120<span class="o">]</span> Bluetooth: SCO socket layer initialized
</span></span><span class="line"><span class="cl"><span class="o">[</span>    0.325395<span class="o">]</span> Bluetooth: HCI UART driver ver 2.3
</span></span><span class="line"><span class="cl"><span class="o">[</span>    0.327116<span class="o">]</span> Bluetooth: RFCOMM socket layer initialized
</span></span><span class="line"><span class="cl"><span class="o">[</span>    0.327117<span class="o">]</span> Bluetooth: RFCOMM ver 1.11
</span></span><span class="line"><span class="cl"><span class="o">[</span>    0.327117<span class="o">]</span> Bluetooth: BNEP <span class="o">(</span>Ethernet Emulation<span class="o">)</span> ver 1.3
</span></span><span class="line"><span class="cl"><span class="o">[</span>    0.327119<span class="o">]</span> Bluetooth: BNEP socket layer initialized
</span></span><span class="line"><span class="cl"><span class="o">[</span>    0.327119<span class="o">]</span> Bluetooth: HIDP <span class="o">(</span>Human Interface Emulation<span class="o">)</span> ver 1.2
</span></span><span class="line"><span class="cl"><span class="o">[</span>    0.327120<span class="o">]</span> Bluetooth: HIDP socket layer initialized
</span></span></code></pre></div><h3 id="wrapping-up">Wrapping up</h3>
<p>If you&rsquo;re not a relatively up-to-date distro, you might need to make some more manual adjustments before everything works. Open up <code>/etc/pulse/default.pa</code> in any editor with root access (so you can write your changes back), then look for <code>module-bluetooth-discover</code>. In my version of the file, I have this:</p>
<pre tabindex="0"><code class="language-pa" data-lang="pa">.ifexists module-bluetooth-discover.so
load-module module-bluetooth-discover
.endif
</code></pre><p>It means that if the module is discovered, it will be loaded. On older versions this might just be <code># load-module module-bluetooth-discover</code>. In that case, uncomment the line.</p>
<p>Next, open up <code>/usr/bin/start-pulseaudio-x11</code> in the same way. Look for this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> x<span class="s2">&#34;</span><span class="nv">$SESSION_MANAGER</span><span class="s2">&#34;</span> !<span class="o">=</span> x <span class="o">]</span> <span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    /usr/bin/pactl load-module module-x11-xsmp <span class="s2">&#34;display=</span><span class="nv">$DISPLAY</span><span class="s2"> xauthority=</span><span class="nv">$XAUTHORITY</span><span class="s2"> session_manager=</span><span class="nv">$SESSION_MANAGER</span><span class="s2">&#34;</span> &gt; /dev/null
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span></code></pre></div><p>Below it, add <code>/usr/bin/pactl load-module module-bluetooth-discover</code> so the final result looks like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> x<span class="s2">&#34;</span><span class="nv">$SESSION_MANAGER</span><span class="s2">&#34;</span> !<span class="o">=</span> x <span class="o">]</span> <span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    /usr/bin/pactl load-module module-x11-xsmp <span class="s2">&#34;display=</span><span class="nv">$DISPLAY</span><span class="s2"> xauthority=</span><span class="nv">$XAUTHORITY</span><span class="s2"> session_manager=</span><span class="nv">$SESSION_MANAGER</span><span class="s2">&#34;</span> &gt; /dev/null
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">/usr/bin/pactl load-module module-bluetooth-discover
</span></span></code></pre></div><p>This will manually load the module when X11 triggers PulseAudio init. This should ideally not be required so you can try without this change, but it won&rsquo;t break anything if you add it anyway.</p>
<p>Once done, reboot your computer and you should be able to pair and connect to devices and play audio through them.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Simple tricks for faster Rust programs</title>
      <link>https://msfjarvis.dev/posts/simple-tricks-for-faster-rust-programs/</link>
      <pubDate>Sun, 05 Jul 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/simple-tricks-for-faster-rust-programs/</guid>
      <description>Rust programs are pretty fast on their own, but you can slightly augment their performance with some simple tricks.</description>
      <content:encoded><![CDATA[<p>Rust is <em>pretty</em> fast. Let&rsquo;s get that out of the way. But sometimes, <em>pretty</em> fast is not fast enough.</p>
<p>Fortunately, it&rsquo;s also <em>pretty</em> easy to slightly improve the performance of your Rust binaries with minimal code changes. I&rsquo;m gonna go over some of these tricks that I&rsquo;ve picked up from many sources across the web (I&rsquo;ll post a small list of <strong>very</strong> good blogs run by smart Rustaceans who cover interesting Rust related things).</p>
<h2 id="turn-on-full-lto">Turn on full LTO</h2>
<p>Rust by default runs a &ldquo;thin&rdquo; LTO pass across each individual <a href="https://doc.rust-lang.org/rustc/codegen-options/index.html#codegen-units">codegen unit</a>. This can be optimized with a very simple addition to your <code>Cargo.toml</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">profile</span><span class="p">.</span><span class="nx">release</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">codegen-units</span> <span class="p">=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"><span class="nx">lto</span> <span class="p">=</span> <span class="s2">&#34;fat&#34;</span>
</span></span></code></pre></div><p>This makes the following changes to the <code>release</code> profile:</p>
<ul>
<li>Forces <code>rustc</code> to build the entire crate as a single unit, which lets LLVM make smarter decisions about optimization thanks to all the code being together.</li>
<li>Switches LTO to the <code>fat</code> variant. In <code>fat</code> mode, LTO will perform <a href="https://doc.rust-lang.org/rustc/codegen-options/index.html#lto">optimization across the entire dependency graph</a> as opposed to the default option of doing it just to the local crate.</li>
</ul>
<h2 id="use-a-different-memory-allocator">Use a different memory allocator</h2>
<p>Some time ago, Rust switched from using <code>jemalloc</code> on all platforms to the OS-native allocator. This caused serious performance regressions in many programs like <a href="https://github.com/sharkdp/fd">fd</a>. To switch back to <code>jemalloc</code>, check out <a href="https://github.com/sharkdp/fd/pull/481">this</a> PR for the changes required.</p>
<p>Note that this alone is not guaranteed to be helpful, and a lot of programs see little to no benefit, so please run your own benchmarks with <a href="https://github.com/sharkdp/hyperfine">hyperfine</a> to confirm whether or not it helped you.</p>
<h2 id="cows">Cows!</h2>
<p>Rustaceans <a href="https://www.reddit.com/r/rust/comments/8o1pxh/the_secret_life_of_cows/">love their cows</a>, and it&rsquo;s one of the most underrated APIs in the Rust standard library. It&rsquo;s claim to fame is relatively simple - it&rsquo;s a smart copy-on-write pointer. Or well, a smart clone-on-write pointer, as copy means something different in Rust as opposed to other languages.</p>
<p>Given a data wrapped in a <code>std::borrow::Cow</code>, you can avoid cloning the data if you only want immutable read access, which saves memory and improves runtime performance as well. Over a large codebase, these savings pile up to create a noticeable enough difference. Here&rsquo;s an example from the Rust standard library that explains this well.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-rust" data-lang="rust"><span class="line"><span class="cl"><span class="k">use</span><span class="w"> </span><span class="n">std</span>::<span class="n">borrow</span>::<span class="n">Cow</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">fn</span> <span class="nf">abs_all</span><span class="p">(</span><span class="n">input</span>: <span class="kp">&amp;</span><span class="nc">mut</span><span class="w"> </span><span class="n">Cow</span><span class="o">&lt;</span><span class="p">[</span><span class="kt">i32</span><span class="p">]</span><span class="o">&gt;</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="mi">0</span><span class="o">..</span><span class="n">input</span><span class="p">.</span><span class="n">len</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kd">let</span><span class="w"> </span><span class="n">v</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">input</span><span class="p">[</span><span class="n">i</span><span class="p">];</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="n">v</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="c1">// Clones into a vector if not already owned.
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">input</span><span class="p">.</span><span class="n">to_mut</span><span class="p">()[</span><span class="n">i</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">-</span><span class="n">v</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// No clone occurs because `input` doesn&#39;t need to be mutated.
</span></span></span><span class="line"><span class="cl"><span class="kd">let</span><span class="w"> </span><span class="n">slice</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">];</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">let</span><span class="w"> </span><span class="k">mut</span><span class="w"> </span><span class="n">input</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Cow</span>::<span class="n">from</span><span class="p">(</span><span class="o">&amp;</span><span class="n">slice</span><span class="p">[</span><span class="o">..</span><span class="p">]);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">abs_all</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span><span class="w"> </span><span class="n">input</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// Clone occurs because `input` needs to be mutated.
</span></span></span><span class="line"><span class="cl"><span class="kd">let</span><span class="w"> </span><span class="n">slice</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">];</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">let</span><span class="w"> </span><span class="k">mut</span><span class="w"> </span><span class="n">input</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Cow</span>::<span class="n">from</span><span class="p">(</span><span class="o">&amp;</span><span class="n">slice</span><span class="p">[</span><span class="o">..</span><span class="p">]);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">abs_all</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span><span class="w"> </span><span class="n">input</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// No clone occurs because `input` is already owned.
</span></span></span><span class="line"><span class="cl"><span class="kd">let</span><span class="w"> </span><span class="k">mut</span><span class="w"> </span><span class="n">input</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Cow</span>::<span class="n">from</span><span class="p">(</span><span class="fm">vec!</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">]);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">abs_all</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span><span class="w"> </span><span class="n">input</span><span class="p">);</span><span class="w">
</span></span></span></code></pre></div><h1 id="references">References</h1>
<ul>
<li>Pascal Hertleif&rsquo;s <a href="https://deterministic.space/">blog</a> - He&rsquo;s a very popular and active Rust developer and writes amazing, insightful articles.</li>
<li>Amos Wenger&rsquo;s <a href="https://fasterthanli.me">blog</a> - Amos&rsquo; articles often go over important topics like API design through a comparison angle between Rust and another language to highlight differences and benefits to each approach.</li>
<li>Stjepan Glavina&rsquo;s <a href="https://stjepang.github.io/">blog</a> - He&rsquo;s done a lot of interesting perf-related work including optimising sorting in the stdlib and building async libraries. His writeups for the library work are very intriguing and go into great detail about the process.</li>
</ul>
]]></content:encoded>
    </item>
    
    <item>
      <title>How Cloudflare proxies CNAME records</title>
      <link>https://msfjarvis.dev/posts/how-cloudflare-proxies-cname-records/</link>
      <pubDate>Fri, 08 May 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/how-cloudflare-proxies-cname-records/</guid>
      <description>Everybody probably understands how Cloudflare proxies A/AAAA records, but how it proxies CNAME records is also pretty interesting. Let&amp;rsquo;s dive into how that happens and why it can often break other products that need you to set CNAME records.</description>
      <content:encoded><![CDATA[<p>As people who&rsquo;ve read my previous post would know, I recently started using <a href="https://purelymail.com/">Purelymail</a> for my email needs (the how and why of it can be found <a href="/posts/switching-my-email-to-purelymail/">here</a>). I also mentioned there, that Cloudflare&rsquo;s proxy-by-default nature caused Purelymail to not detect my CNAME settings and disabling the proxy did the job. I contacted Purelymail&rsquo;s Scott about this and he eventually pushed a fix out that *should* have fixed it, but since he did not have a Cloudflare account, he couldn&rsquo;t verify this exact case.</p>
<p>Well, the fix didn&rsquo;t work.</p>
<p>This made me wonder, <em>why?</em> I trust that Scott is more aware of what he&rsquo;s doing than I am so the fix must have been legitimate and that something is special about Cloudflare&rsquo;s handling of this. So I did some testing! (yes I still use <a href="https://stackexchange.github.io/dnscontrol/">dnscontrol</a>)</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gh">diff --git a/dnsconfig.js b/dnsconfig.js
</span></span></span><span class="line"><span class="cl"><span class="gh">index f5fd1836f8ec..a53bab70de84 100644
</span></span></span><span class="line"><span class="cl"><span class="gd">--- a/dnsconfig.js
</span></span></span><span class="line"><span class="cl"><span class="gi">+++ b/dnsconfig.js
</span></span></span><span class="line"><span class="cl"><span class="gu">@@ -24,6 +24,8 @@ var DEV_RECORDS = [
</span></span></span><span class="line"><span class="cl">     CNAME(&#39;purelymail2._domainkey&#39;, &#39;key2._dkimroot.purelymail.com.&#39;, CF_PROXY_OFF),
</span></span><span class="line"><span class="cl">     CNAME(&#39;purelymail3._domainkey&#39;, &#39;key3._dkimroot.purelymail.com.&#39;, CF_PROXY_OFF),
</span></span><span class="line"><span class="cl">     CNAME(&#39;status&#39;, &#39;stats.uptimerobot.com.&#39;, CF_PROXY_OFF),
</span></span><span class="line"><span class="cl"><span class="gi">+    CNAME(&#39;test_domain_no_proxy&#39;, &#39;msfjarvis.github.io.&#39;, CF_PROXY_OFF),
</span></span></span><span class="line"><span class="cl"><span class="gi">+    CNAME(&#39;test_domain_with_proxy&#39;, &#39;msfjarvis.github.io.&#39;),
</span></span></span><span class="line"><span class="cl">     MX(&#39;@&#39;, 50, &#39;mailserver.purelymail.com.&#39;),
</span></span><span class="line"><span class="cl">     TXT(&#39;@&#39;, &#39;v=spf1 include:_spf.purelymail.com ~all&#39;),
</span></span><span class="line"><span class="cl">     TXT(&#39;@&#39;, &#39;purelymail_ownership_proof=0xd34db33f&#39;),
</span></span></code></pre></div><p>Running <a href="https://linux.die.net/man/1/dig">dig</a> on both the subdomains, I spotted something interesting.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ dig @1.1.1.1 test_domain_no_proxy.msfjarvis.dev
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl"><span class="p">;;</span> ANSWER SECTION:
</span></span><span class="line"><span class="cl">test_domain_no_proxy.msfjarvis.dev. 289	IN CNAME msfjarvis.github.io.
</span></span><span class="line"><span class="cl">msfjarvis.github.io.	3589	IN	A	185.199.109.153
</span></span><span class="line"><span class="cl">msfjarvis.github.io.	3589	IN	A	185.199.111.153
</span></span><span class="line"><span class="cl">msfjarvis.github.io.	3589	IN	A	185.199.108.153
</span></span><span class="line"><span class="cl">msfjarvis.github.io.	3589	IN	A	185.199.110.153
</span></span><span class="line"><span class="cl">...
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ dig @1.1.1.1 test_domain_with_proxy.msfjarvis.dev
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl"><span class="p">;;</span> ANSWER SECTION:
</span></span><span class="line"><span class="cl">test_domain_with_proxy.msfjarvis.dev. <span class="m">300</span> IN A	104.28.14.93
</span></span><span class="line"><span class="cl">test_domain_with_proxy.msfjarvis.dev. <span class="m">300</span> IN A	104.28.15.93
</span></span><span class="line"><span class="cl">...
</span></span></code></pre></div><p>The proxied CNAME record isn&rsquo;t actually a CNAME after all! Cloudflare creates an A record for it and handles the redirection internally. This makes the CNAME aspect of the record opaque to DNS lookups which in turn trips software like Purelymail&rsquo;s backend. I&rsquo;ve reported my findings to Scott and am awaiting his response.</p>
<p>And that&rsquo;s it! Nothing too fancy, just something I found kinda weird.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Switching my email to Purelymail</title>
      <link>https://msfjarvis.dev/posts/switching-my-email-to-purelymail/</link>
      <pubDate>Mon, 13 Apr 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/switching-my-email-to-purelymail/</guid>
      <description>I recently moved from forwarding my email through Google to hosting it through Purelymail.com. Here are some thoughts about the process and the motivation behind it</description>
      <content:encoded><![CDATA[<p>Email is a very crucial part of my workflow, and I enjoy using it (and also why I&rsquo;m beyond excited for what Basecamp has in store with <a href="https://hey.com">hey.com</a>). I have switched emails a couple times over the many years I have had an internet presence, finally settling on <a href="mailto:me@msfjarvis.dev">me@msfjarvis.dev</a> when I bought my domain. There began the problem.</p>
<p>I attempt to self-host things when reasonable, to retain some control and not have a single point of failure outside my control that would lock me out. With email, that part is a constant, uphill battle against spam filters to ensure your domain doesn&rsquo;t land in a big filter list that will then start trashing all your email and make life hard. Due to this, I never self-hosted email, instead choosing to forward it through Google Domains (the registrar for this domain) to my existing Google account. While this is a very reliable approach, it still involves depending heavily on my Google account. This has proven to be a problem in many ways, including being locked out after opting into Advanced Protection and people&rsquo;s accounts being banned for a number of reasons completely unrelated to email. If something like this were to happen to me, I would lose both my Google as well as my domain email instantly. A very scary position to be in for anybody.</p>
<p>A couple days ago, <a href="https://twitter.com/JakeWharton">Jake Wharton</a> retweeted a blog post from <a href="https://twitter.com/rolisz">Roland Szabo</a> titled <a href="https://rolisz.ro/2020/04/11/moving-away-from-gmail/">&lsquo;Moving away from GMail&rsquo;</a>. I read through it, looked at PurelyMail, and was convinced that it was really the solution for my little problem. I am a big believer in paying in dollaroos rather than data so I really loved the transparency behind pricing, data use, infrastructure and just about everything else. Signed up!</p>
<h2 id="migration">Migration</h2>
<p>Like any other email provider, all you need to configure for PurelyMail to work is DNS. I use Cloudflare for my sites, so there was nothing to do on the Google Domains side of things. I left the forwarding setup as-is to allow any lagging DNS resolvers to still be able to get email to me, even if its to my Google account. I hope to get rid of that setting in the near future since I believe the change will have propagated by then. I maintain my DNS settings under a git repository, using StackExchange&rsquo;s excellent <a href="http://stackexchange.github.io/dnscontrol/">dnscontrol</a> tool. DNSControl operates on a JS-like syntax that is parsed, evaluated and then used to publish to the DNS provider of choice. Neat stuff! The changes required looked something like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gh">diff --git dnsconfig.js dnsconfig.js
</span></span></span><span class="line"><span class="cl"><span class="gh">index 29b8d1a927ab..01ea2af1d448 100644
</span></span></span><span class="line"><span class="cl"><span class="gd">--- dnsconfig.js
</span></span></span><span class="line"><span class="cl"><span class="gi">+++ dnsconfig.js
</span></span></span><span class="line"><span class="cl"><span class="gu">@@ -6,7 +6,7 @@
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> var REG_NONE = NewRegistrar(&#39;none&#39;, &#39;NONE&#39;);
</span></span><span class="line"><span class="cl"> var DNS_CF = NewDnsProvider(&#39;cloudflare&#39;, &#39;CLOUDFLAREAPI&#39;);
</span></span><span class="line"><span class="cl"><span class="gi">+var CF_PROXY_OFF = {&#39;cloudflare_proxy&#39;: &#39;off&#39;};     // Proxy disabled.
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">@@ -18,25 +18,17 @@ var RECORDS = [
</span></span></span><span class="line"><span class="cl"><span class="gi">+    CNAME(&#39;_dmarc&#39;, &#39;_dmarcroot.purelymail.com.&#39;, CF_PROXY_OFF),
</span></span></span><span class="line"><span class="cl"><span class="gi">+    CNAME(&#39;purelymail1._domainkey&#39;, &#39;key1._dkimroot.purelymail.com.&#39;, CF_PROXY_OFF),
</span></span></span><span class="line"><span class="cl"><span class="gi">+    CNAME(&#39;purelymail2._domainkey&#39;, &#39;key2._dkimroot.purelymail.com.&#39;, CF_PROXY_OFF),
</span></span></span><span class="line"><span class="cl"><span class="gi">+    CNAME(&#39;purelymail3._domainkey&#39;, &#39;key3._dkimroot.purelymail.com.&#39;, CF_PROXY_OFF),
</span></span></span><span class="line"><span class="cl"><span class="gd">-    MX(&#39;@&#39;, 10, &#39;alt1.gmr-smtp-in.l.google.com.&#39;, TTL(&#39;1d&#39;)),
</span></span></span><span class="line"><span class="cl"><span class="gd">-    MX(&#39;@&#39;, 20, &#39;alt2.gmr-smtp-in.l.google.com.&#39;, TTL(&#39;1d&#39;)),
</span></span></span><span class="line"><span class="cl"><span class="gd">-    MX(&#39;@&#39;, 30, &#39;alt3.gmr-smtp-in.l.google.com.&#39;, TTL(&#39;1d&#39;)),
</span></span></span><span class="line"><span class="cl"><span class="gd">-    MX(&#39;@&#39;, 40, &#39;alt4.gmr-smtp-in.l.google.com.&#39;, TTL(&#39;1d&#39;)),
</span></span></span><span class="line"><span class="cl"><span class="gd">-    MX(&#39;@&#39;, 5, &#39;gmr-smtp-in.l.google.com.&#39;, TTL(&#39;1d&#39;)),
</span></span></span><span class="line"><span class="cl"><span class="gd">-    TXT(&#39;@&#39;, &#39;v=spf1 include:_spf.google.com ~all&#39;, TTL(&#39;3600s&#39;)),
</span></span></span><span class="line"><span class="cl"><span class="gi">+    MX(&#39;@&#39;, 50, &#39;mailserver.purelymail.com.&#39;),
</span></span></span><span class="line"><span class="cl"><span class="gi">+    TXT(&#39;@&#39;, &#39;v=spf1 include:_spf.purelymail.com ~all&#39;),
</span></span></span><span class="line"><span class="cl"><span class="gi">+    TXT(&#39;@&#39;, &#39;purelymail_ownership_proof=**redacated**&#39;),
</span></span></span><span class="line"><span class="cl"> ];
</span></span></code></pre></div><p>The only &lsquo;unexpected&rsquo; change I had to make was to disable Cloudflare&rsquo;s proxy feature for the CNAME records. Once that was done, PurelyMail was instantly able to verify all DNS records and I was in business.</p>
<h2 id="pros-and-cons-of-the-switch">Pros and Cons of the switch</h2>
<p>I&rsquo;ve been on PurelyMail for about a day now and poked around enough to have a comprehensive idea of what&rsquo;s different from my usual GMail flow, so let&rsquo;s get into that.</p>
<h3 id="pros">Pros</h3>
<h4 id="you-pay-for-it">You pay for it</h4>
<p>By now everybody must have realized a simple fact: If you&rsquo;re not paying, you&rsquo;re the product. I do not wish to be a product. Your hard-earned money is more likely to keep companies from being shady than your emails. PurelyMail is a one man operation, which makes it more trustworthy to me than Google&rsquo;s massive scale. Google does not care for a single user, PurelyMail will.</p>
<h4 id="transparency">Transparency</h4>
<p>PurelyMail tells you upfront about what they charge and how they arrive at that number. There is no contract period, and if you wish to have fine grained control over what you pay, you can use their advanced pricing section to calculate your costs based on your exact needs. The website is straightforward and to the point, there is no glossy advertising to obscure flaws, and their security practices are all <a href="https://purelymail.com/docs/security">documented</a> on their site, front and center.</p>
<h4 id="failsafe">Failsafe</h4>
<p>My GMail is tied to my Google account, which means anything that flags my Google account will bring down my ability to have email. This is a scary position to be in. Having my email separate from my Google account frees me from that looming danger.</p>
<h4 id="easy-export">Easy export</h4>
<p>PurelyMail has a tool called <a href="https://purelymail.com/docs/mailPort"><code>mailPort</code></a> that lets you move email between PurelyMail and other providers. You can bring your entire mailbox to PurelyMail when switching to it, or back to wherever you go next should it not feel sufficient for your needs. No questions asked, and no bullshit. It just works.</p>
<h4 id="no-client-lock-in">No client lock-in</h4>
<p>Because PurelyMail has no bells and whistles, you won&rsquo;t be penalized on the feature side of things if you use one client compared to another. Things stay consistent.</p>
<h3 id="cons">Cons</h3>
<h4 id="you-pay-for-it-1">You pay for it</h4>
<p>I am in a fortunate position where I can pay for things solely based on principle, without having to worry <em>too</em> much. Not everybody is similarly blessed, or you may simply have technical issues with being able to pay online for internet things. Stripe and PayPal are not available globally, and fees are often insane. I completely understand.</p>
<h4 id="roundcube-is-great-but-it-aint-no-gmail">Roundcube is great, but it ain&rsquo;t no GMail</h4>
<p>PurelyMail uses the Roundcube frontend for its webmail offering, with a couple extra themes. It&rsquo;s not the prettiest, and does not have a lot of bells and whistles that you might get accustomed to from GMail. The change is a bit rough honestly, but the pros certainly outweigh the cons. On the bright side, its easier to influence product direction at PurelyMail, so get on the issue tracker and request or vote for features!</p>
<h4 id="no-dedicated-client">No dedicated client</h4>
<p>Not having a specialized client unfortunately also means that you&rsquo;ll have to shop around for what works. I still use the GMail mobile app, but K-9 Mail is also pretty decent.</p>
<h2 id="conclusion">Conclusion</h2>
<p>I have begun moving my various accounts to my domain mail as and when they remind me of their existence (55 left still, if my <a href="https://passwordstore.org/">pass</a> repository is to be believed), and hope to eventually be able to get by without the pinned GMail tab in my browser :)</p>
<p>PurelyMail has proven to be an excellent platform so far. Support has been swift and helpful, and I haven&rsquo;t had any bad surprises. I hope to be a content user for as long as I possibly can :)</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Dagger the easy way - Part 2</title>
      <link>https://msfjarvis.dev/posts/dagger-the-easy-way--part-2/</link>
      <pubDate>Fri, 06 Mar 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/dagger-the-easy-way--part-2/</guid>
      <description>Let&amp;rsquo;s extend the &amp;ldquo;scope&amp;rdquo; of these tutorials :)</description>
      <content:encoded><![CDATA[<p>Welcome back! In this post I&rsquo;m taking a bit of detour from my planned schedule to write about <strong>scoping</strong>. We&rsquo;ll <em>definitely</em> cover constructor injection in the next part :)</p>
<blockquote>
<p>All the code from this post is available on GitHub: <a href="https://github.com/msfjarvis/dagger-the-easy-way/commits/part-2">msfjarvis/dagger-the-easy-way</a></p>
</blockquote>
<p>Dagger 2 provides <code>@Scope</code> as a mechanism to handle scoping. Scoping allows you to keep an object instance for the duration of your scope. This means that no matter how many times the object is requested from Dagger, it returns the same instance.</p>
<h2 id="default-scopes">Default scopes</h2>
<p>In the previous tutorial, we looked at <em>two</em> scopes, namely <code>@Singleton</code> and <code>@Reusable</code>. Singleton does what its name suggests, and &ldquo;caches&rdquo; the dependency instance for the lifecycle of the <code>@Component</code>, and Reusable tells Dagger that while we&rsquo;d prefer that a cached instance be used, we&rsquo;re fine if Dagger needs to create another one. The new Dagger 2 <a href="https://dagger.dev/users-guide">user guide</a> does a pretty good job differentiating between Singleton, Reusable and unscoped dependencies which I&rsquo;ll reproduce here.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="c1">// It doesn&#39;t matter how many scoopers we use, but don&#39;t waste them.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nd">@Reusable</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">CoffeeScooper</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nd">@Inject</span><span class="w"> </span><span class="n">CoffeeScooper</span><span class="p">()</span><span class="w"> </span><span class="p">{}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nd">@Module</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">CashRegisterModule</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nd">@Provides</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c1">// DON&#39;T DO THIS! You do care which register you put your cash in.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c1">// Use a specific scope instead.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nd">@Reusable</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="kd">static</span><span class="w"> </span><span class="n">CashRegister</span><span class="w"> </span><span class="nf">badIdeaCashRegister</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">CashRegister</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// DON&#39;T DO THIS! You really do want a new filter each time, so this</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// should be unscoped.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nd">@Reusable</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">CoffeeFilter</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nd">@Inject</span><span class="w"> </span><span class="n">CoffeeFilter</span><span class="p">()</span><span class="w"> </span><span class="p">{}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><h2 id="why-do-we-need-scopes">Why do we need scopes</h2>
<p>I&rsquo;ll do a small demo to show the difference between unscoped and singleton dependencies, then we&rsquo;ll move on to defining our own scopes.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="c1">// AppComponent.kt
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">data</span> <span class="k">class</span> <span class="nc">Counter</span><span class="p">(</span><span class="k">val</span> <span class="py">name</span><span class="p">:</span> <span class="n">String</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@Component</span><span class="p">(</span><span class="n">modules</span> <span class="p">=</span> <span class="p">[</span><span class="n">AppModule</span><span class="o">::</span><span class="k">class</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="k">interface</span> <span class="nc">AppComponent</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">fun</span> <span class="nf">getCounter</span><span class="p">():</span> <span class="n">Counter</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@Module</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">AppModule</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">private</span> <span class="k">var</span> <span class="py">index</span> <span class="p">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nd">@Provides</span>
</span></span><span class="line"><span class="cl">  <span class="k">fun</span> <span class="nf">provideCounter</span><span class="p">():</span> <span class="n">Counter</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">index</span><span class="o">++</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">Counter</span><span class="p">(</span><span class="s2">&#34;Counter </span><span class="si">$index</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>These dependencies are all unscoped, along with the <code>AppComponent</code>. Knowing what we do about unscoped elements in a Dagger graph, predict the output of the following code:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">CounterApplication</span> <span class="p">:</span> <span class="n">Application</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">private</span> <span class="k">val</span> <span class="py">TAG</span> <span class="p">=</span> <span class="s2">&#34;CounterApplication&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">override</span> <span class="k">fun</span> <span class="nf">onCreate</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">super</span><span class="p">.</span><span class="n">onCreate</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">val</span> <span class="py">appComponent</span> <span class="p">=</span> <span class="nc">DaggerAppComponent</span><span class="p">.</span><span class="n">builder</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">      <span class="p">.</span><span class="n">appModule</span><span class="p">(</span><span class="n">AppModule</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">      <span class="p">.</span><span class="n">build</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="nc">Log</span><span class="p">.</span><span class="n">d</span><span class="p">(</span><span class="n">TAG</span><span class="p">,</span> <span class="n">appComponent</span><span class="p">.</span><span class="n">getCounter</span><span class="p">().</span><span class="n">name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nc">Log</span><span class="p">.</span><span class="n">d</span><span class="p">(</span><span class="n">TAG</span><span class="p">,</span> <span class="n">appComponent</span><span class="p">.</span><span class="n">getCounter</span><span class="p">().</span><span class="n">name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Running this on a device will print the following in your logcat</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="n">D</span><span class="p">/</span><span class="n">CounterApplication</span><span class="p">:</span> <span class="n">Counter</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"><span class="n">D</span><span class="p">/</span><span class="n">CounterApplication</span><span class="p">:</span> <span class="n">Counter</span> <span class="mi">2</span>
</span></span></code></pre></div><p>Totally expected, because unscoped dependencies have no lifecycle in the component, and hence are created every time you ask for one. Let&rsquo;s make them all into Singletons and see how that changes things.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"> data class Counter(val name: String)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gi">+@Singleton
</span></span></span><span class="line"><span class="cl"> @Component(modules = [AppModule::class])
</span></span><span class="line"><span class="cl"> interface AppComponent {
</span></span><span class="line"><span class="cl">   fun getCounter(): Counter
</span></span><span class="line"><span class="cl"><span class="gu">@@ -12,6 +13,7 @@ class AppModule {
</span></span></span><span class="line"><span class="cl">   private var index = 0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">   @Provides
</span></span><span class="line"><span class="cl"><span class="gi">+  @Singleton
</span></span></span><span class="line"><span class="cl">   fun provideCounter(): Counter {
</span></span><span class="line"><span class="cl">     index++
</span></span><span class="line"><span class="cl">     return Counter(&#34;Counter $index&#34;)
</span></span></code></pre></div><p>Running the same code again, we get</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="n">D</span><span class="p">/</span><span class="n">CounterApplication</span><span class="p">:</span> <span class="n">Counter</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"><span class="n">D</span><span class="p">/</span><span class="n">CounterApplication</span><span class="p">:</span> <span class="n">Counter</span> <span class="mi">1</span>
</span></span></code></pre></div><p>Notice that we were handed the same instance. This is the power of scoping. It lets us have singletons within the defined scope.</p>
<p>Like Arun mentioned in the <a href="/posts/dagger-the-easy-way--part-1/#setting-up-the-object-graph">additional notes</a> for the previous article, ensuring a singleton Component stays that way is the user&rsquo;s job. If you initialize the component again within the same scope, the new component instance will have a new set of instances. That is part of why we store our component in the <a href="https://developer.android.com/reference/android/app/Application.html">Application</a> class, because it is the singleton for our apps.</p>
<h2 id="creating-our-own-scopes">Creating our own scopes</h2>
<p>In its most basic form, a scope is an annotation class that itself has two annotations, <code>@Scope</code> and <code>@Retention</code>. Assuming we follow an MVP architecture (purely for nomenclature purposes, scoping is not necessarily tied to your architecture), let&rsquo;s create a scope for our <code>CounterPresenter</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="nd">@Scope</span>
</span></span><span class="line"><span class="cl"><span class="nd">@Retention</span><span class="p">(</span><span class="nc">AnnotationRetention</span><span class="p">.</span><span class="n">RUNTIME</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">annotation</span> <span class="k">class</span> <span class="nc">CounterScreenScope</span>
</span></span></code></pre></div><p>Putting this annotation together with our presenter and our component, we finally get this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="nd">@Scope</span>
</span></span><span class="line"><span class="cl"><span class="nd">@Retention</span><span class="p">(</span><span class="nc">AnnotationRetention</span><span class="p">.</span><span class="n">RUNTIME</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">annotation</span> <span class="k">class</span> <span class="nc">CounterScreenScope</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">data</span> <span class="k">class</span> <span class="nc">Counter</span><span class="p">(</span><span class="k">val</span> <span class="py">name</span><span class="p">:</span> <span class="n">String</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">CounterPresenter</span><span class="p">(</span><span class="k">val</span> <span class="py">counter</span><span class="p">:</span> <span class="n">Counter</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@Module</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">CounterScreenModule</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nd">@Provides</span>
</span></span><span class="line"><span class="cl">  <span class="nd">@CounterScreenScope</span>
</span></span><span class="line"><span class="cl">  <span class="k">fun</span> <span class="nf">provideCounterPresenter</span><span class="p">(</span><span class="n">counter</span><span class="p">:</span> <span class="n">Counter</span><span class="p">):</span> <span class="n">CounterPresenter</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">CounterPresenter</span><span class="p">(</span><span class="n">counter</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@CounterScreenScope</span>
</span></span><span class="line"><span class="cl"><span class="nd">@Subcomponent</span><span class="p">(</span><span class="n">modules</span> <span class="p">=</span> <span class="p">[</span><span class="n">CounterScreenModule</span><span class="o">::</span><span class="k">class</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="k">interface</span> <span class="nc">CounterScreenComponent</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">fun</span> <span class="nf">inject</span><span class="p">(</span><span class="n">counterActivity</span><span class="p">:</span> <span class="n">MainActivity</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@Singleton</span>
</span></span><span class="line"><span class="cl"><span class="nd">@Component</span><span class="p">(</span><span class="n">modules</span> <span class="p">=</span> <span class="p">[</span><span class="n">AppModule</span><span class="o">::</span><span class="k">class</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="k">interface</span> <span class="nc">AppComponent</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">fun</span> <span class="nf">counterScreenComponent</span><span class="p">(</span><span class="n">counterScreenModule</span><span class="p">:</span> <span class="n">CounterScreenModule</span><span class="p">):</span> <span class="n">CounterScreenComponent</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@Module</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">AppModule</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">private</span> <span class="k">var</span> <span class="py">index</span> <span class="p">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">  <span class="nd">@Provides</span>
</span></span><span class="line"><span class="cl">  <span class="k">fun</span> <span class="nf">getCounter</span><span class="p">():</span> <span class="n">Counter</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">index</span><span class="o">++</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">Counter</span><span class="p">(</span><span class="s2">&#34;Counter </span><span class="si">$index</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Phew, a lot happened there. Let&rsquo;s break it down.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">CounterPresenter</span><span class="p">(</span><span class="k">val</span> <span class="py">counter</span><span class="p">:</span> <span class="n">Counter</span><span class="p">)</span>
</span></span></code></pre></div><p>This is simply a class that represents our presenter. We don&rsquo;t care much for implementation details here, so the class does nothing.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="nd">@Module</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">CounterScreenModule</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nd">@Provides</span>
</span></span><span class="line"><span class="cl">  <span class="nd">@CounterScreenScope</span>
</span></span><span class="line"><span class="cl">  <span class="k">fun</span> <span class="nf">provideCounterPresenter</span><span class="p">(</span><span class="n">counter</span><span class="p">:</span> <span class="n">Counter</span><span class="p">):</span> <span class="n">CounterPresenter</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">CounterPresenter</span><span class="p">(</span><span class="n">counter</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><code>CounterScreenModule</code> holds the provider method for our presenter. The method is annotated with <code>@CounterScreenScope</code> to indicate that we want to scope its lifetime to our screen. Rather than being an <code>object</code> like our <code>AppModule</code>, it&rsquo;s a <code>class</code> because we need to instantiate it manually later.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="nd">@Singleton</span>
</span></span><span class="line"><span class="cl"><span class="nd">@Component</span><span class="p">(</span><span class="n">modules</span> <span class="p">=</span> <span class="p">[</span><span class="n">AppModule</span><span class="o">::</span><span class="k">class</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="k">interface</span> <span class="nc">AppComponent</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">fun</span> <span class="nf">counterScreenComponent</span><span class="p">(</span><span class="n">counterScreenModule</span><span class="p">:</span> <span class="n">CounterScreenModule</span><span class="p">):</span> <span class="n">CounterScreenComponent</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>To our <code>AppComponent</code>, we&rsquo;ve simply added a method to provide the <code>CounterScreenComponent</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="nd">@CounterScreenScope</span>
</span></span><span class="line"><span class="cl"><span class="nd">@Subcomponent</span><span class="p">(</span><span class="n">modules</span> <span class="p">=</span> <span class="p">[</span><span class="n">CounterScreenModule</span><span class="o">::</span><span class="k">class</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="k">interface</span> <span class="nc">CounterScreenComponent</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">fun</span> <span class="nf">inject</span><span class="p">(</span><span class="n">counterActivity</span><span class="p">:</span> <span class="n">MainActivity</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><code>CounterScreenComponent</code> is a <a href="https://dagger.dev/api/latest/dagger/Subcomponent.html">Subcomponent</a>. In simple, OOP terms, it&rsquo;s a Component that inherits from another Component. A Subcomponent can only have one parent, and the Subcomponent doesn&rsquo;t get to pick who, much like real life :P</p>
<p>The parent Component is responsible for ensuring that all the dependencies of a Subcomponent are available, other than modules.</p>
<h2 id="putting-it-all-together">Putting it all together</h2>
<p>After setting up our Dagger graph, instantiating everything becomes pretty easy.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">MainActivity</span> <span class="p">:</span> <span class="n">AppCompatActivity</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nd">@Inject</span>
</span></span><span class="line"><span class="cl">  <span class="k">lateinit</span> <span class="k">var</span> <span class="py">presenter</span><span class="p">:</span> <span class="n">CounterPresenter</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">override</span> <span class="k">fun</span> <span class="nf">onCreate</span><span class="p">(</span><span class="n">savedInstanceState</span><span class="p">:</span> <span class="n">Bundle</span><span class="p">?)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">super</span><span class="p">.</span><span class="n">onCreate</span><span class="p">(</span><span class="n">savedInstanceState</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">setContentView</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">layout</span><span class="p">.</span><span class="n">activity_main</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="k">val</span> <span class="py">appComponent</span> <span class="p">=</span> <span class="nc">DaggerAppComponent</span><span class="p">.</span><span class="n">builder</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="p">.</span><span class="n">appModule</span><span class="p">(</span><span class="n">AppModule</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="p">.</span><span class="n">build</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="k">val</span> <span class="py">counterScreenComponent</span> <span class="p">=</span> <span class="n">appComponent</span>
</span></span><span class="line"><span class="cl">        <span class="p">.</span><span class="n">counterScreenComponent</span><span class="p">(</span><span class="n">CounterScreenModule</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">      <span class="n">counterScreenComponent</span><span class="p">.</span><span class="n">inject</span><span class="p">(</span><span class="k">this</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="nc">Log</span><span class="p">.</span><span class="n">d</span><span class="p">(</span><span class="n">TAG</span><span class="p">,</span> <span class="n">presenter</span><span class="p">.</span><span class="n">counter</span><span class="p">.</span><span class="n">name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">companion</span> <span class="k">object</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">private</span> <span class="k">const</span> <span class="k">val</span> <span class="py">TAG</span> <span class="p">=</span> <span class="s2">&#34;MainActivity&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Thanks to how our graph is laid out, it is very easy to get subcomponent instances from our parent components.</p>
<h2 id="alternative-initialization">Alternative initialization</h2>
<p>We can also use a <code>@Subcomponent.Factory</code> for <code>CounterScreenComponent</code> to initialize it in a fashion similar to our <code>AppComponent</code> from the previous part. The diff from this change goes something like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gh">diff --git app/src/main/java/dev/msfjarvis/daggertutorial/MainActivity.kt app/src/main/java/dev/msfjarvis/daggertutorial/MainActivity.kt
</span></span></span><span class="line"><span class="cl"><span class="gh">index 4271d151da6e..425e8358902c 100644
</span></span></span><span class="line"><span class="cl"><span class="gd">--- app/src/main/java/dev/msfjarvis/daggertutorial/MainActivity.kt
</span></span></span><span class="line"><span class="cl"><span class="gi">+++ app/src/main/java/dev/msfjarvis/daggertutorial/MainActivity.kt
</span></span></span><span class="line"><span class="cl"><span class="gu">@@ -23,7 +23,8 @@ class MainActivity : AppCompatActivity() {
</span></span></span><span class="line"><span class="cl">             .build()
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">         val counterScreenComponent = appComponent
</span></span><span class="line"><span class="cl"><span class="gd">-            .counterScreenComponent(CounterScreenModule())
</span></span></span><span class="line"><span class="cl"><span class="gi">+            .counterScreenComponentFactory
</span></span></span><span class="line"><span class="cl"><span class="gi">+            .create(CounterScreenModule())
</span></span></span><span class="line"><span class="cl">         counterScreenComponent.inject(this)
</span></span><span class="line"><span class="cl">         Log.d(TAG, presenter.counter.name)
</span></span><span class="line"><span class="cl">     }
</span></span><span class="line"><span class="cl"><span class="gh">diff --git app/src/main/java/dev/msfjarvis/daggertutorial/di/AppComponent.kt app/src/main/java/dev/msfjarvis/daggertutorial/di/AppComponent.kt
</span></span></span><span class="line"><span class="cl"><span class="gh">index 2fb831771ee8..72acea6f6f43 100644
</span></span></span><span class="line"><span class="cl"><span class="gd">--- app/src/main/java/dev/msfjarvis/daggertutorial/di/AppComponent.kt
</span></span></span><span class="line"><span class="cl"><span class="gi">+++ app/src/main/java/dev/msfjarvis/daggertutorial/di/AppComponent.kt
</span></span></span><span class="line"><span class="cl"><span class="gu">@@ -1,5 +1,6 @@
</span></span></span><span class="line"><span class="cl"> package dev.msfjarvis.daggertutorial.di
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gi">+import dagger.BindsInstance
</span></span></span><span class="line"><span class="cl"> import dagger.Component
</span></span><span class="line"><span class="cl"> import dagger.Module
</span></span><span class="line"><span class="cl"> import dagger.Provides
</span></span><span class="line"><span class="cl"><span class="gu">@@ -28,12 +29,16 @@ class CounterScreenModule {
</span></span></span><span class="line"><span class="cl"> @Subcomponent(modules = [CounterScreenModule::class])
</span></span><span class="line"><span class="cl"> interface CounterScreenComponent {
</span></span><span class="line"><span class="cl">     fun inject(counterActivity: MainActivity)
</span></span><span class="line"><span class="cl"><span class="gi">+    @Subcomponent.Factory
</span></span></span><span class="line"><span class="cl"><span class="gi">+    interface Factory {
</span></span></span><span class="line"><span class="cl"><span class="gi">+        fun create(@BindsInstance counterScreenModule: CounterScreenModule): CounterScreenComponent
</span></span></span><span class="line"><span class="cl"><span class="gi">+    }
</span></span></span><span class="line"><span class="cl"> }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> @Singleton
</span></span><span class="line"><span class="cl"> @Component(modules = [AppModule::class])
</span></span><span class="line"><span class="cl"> interface AppComponent {
</span></span><span class="line"><span class="cl"><span class="gd">-    fun counterScreenComponent(counterScreenModule: CounterScreenModule): CounterScreenComponent
</span></span></span><span class="line"><span class="cl"><span class="gi">+    val counterScreenComponentFactory: CounterScreenComponent.Factory
</span></span></span><span class="line"><span class="cl"> }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> @Module
</span></span></code></pre></div><h2 id="closing-notes">Closing Notes</h2>
<p>That&rsquo;s it for this tutorial! Scoping is a rather complex concept, and it took me a long (really, really long) time to grasp its concepts and put this together. Its perfectly fine to not understand it immediately, take your time, and refer to one of the reference articles that I used (listed below) to see if maybe their explanations work better for you. Dagger away!</p>
<h3 id="references">References</h3>
<ul>
<li><a href="https://medium.com/tompee/dagger-2-scopes-and-subcomponents-d54d58511781">Dagger 2: Scopes and Subcomponents</a></li>
<li><a href="https://dagger.dev/users-guide">Dagger User&rsquo;s Guide</a></li>
<li><a href="https://mirekstanek.online/dependency-injection-with-dagger-2-custom-scopes/">Dependency injection with Dagger 2 - Custom scopes</a></li>
</ul>
]]></content:encoded>
    </item>
    
    <item>
      <title>Sunsetting Viscerion</title>
      <link>https://msfjarvis.dev/posts/sunsetting-viscerion/</link>
      <pubDate>Sun, 09 Feb 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/sunsetting-viscerion/</guid>
      <description>The Viscerion experiment that started more than a year ago is now coming to an end. Here&amp;rsquo;s what&amp;rsquo;s happening.</description>
      <content:encoded><![CDATA[<p>Viscerion is one of my more known and loved apps that I myself continue to enjoy working on and using. The project started back in 2018 following a short stint with WireGuard working on their own Android app, and is now being shut down.</p>
<blockquote>
<p>TL;DR: The work I have been doing on Viscerion for the past year will become a part of the upstream WireGuard app over the next 6 months under an agreement between me and Jason Donenfeld, the WireGuard creator and lead developer.</p>
</blockquote>
<h2 id="the-story-behind-viscerion">The story behind Viscerion</h2>
<p>When I initially started this project, it was called WireGuard-KT, and as the dumb and literal name suggests, began with me rewriting the app into Kotlin. I was not a huge fan of Kotlin at that point in time, but was eager to learn and this was the perfect opportunity. My ambitions were rather too lofty for the upstream project at that time and they had to let me go from the internship position, presenting the option to pursue everything I had planned, in a personal capacity.</p>
<p>When I was working on the upstream app, I was seeding builds of my staging branches to a group of friends, who also became the first users/testers of WireGuard-KT. They encouraged me to publish the app to the Play Store which has since been unpublished over copyright concerns about the similarity of the name and resulted in the rebranding of the project as Viscerion.</p>
<h2 id="fast-forwarding-to-today">Fast-forwarding to today</h2>
<p>Jason contacted me, extending an invitation to bring my work from Viscerion to upstream under a paid contract which would involve shutting down Viscerion since the reason why it was created in the first place was now void (consider this like Inbox and Gmail but an alternate universe where the most important features weren&rsquo;t being skipped over). After coming to a mutual agreement over what features and changes would be and what would be the process of deprecating Viscerion, I was officially hired and given full push access.</p>
<h2 id="whats-going-to-happen-with-viscerion">What&rsquo;s going to happen with Viscerion</h2>
<p>I have submitted a final <a href="https://github.com/msfjarvis/viscerion/releases/latest">5.2.11</a> release to the <a href="https://play.google.com/store/apps/details?id=me.msfjarvis.viscerion">Play Store</a>, and the repository has been made read-only. The Play Store listing will be unpublished after 60 days and will only be available to existing users. Hearty thanks to every single user of Viscerion that has helped make this experiment a roaring success and to Jason for finally coming around :p</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Creating a continuously deploying static statuspage with GitHub</title>
      <link>https://msfjarvis.dev/posts/creating-a-continuously-deploying-static-statuspage-with-github/</link>
      <pubDate>Wed, 05 Feb 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/creating-a-continuously-deploying-static-statuspage-with-github/</guid>
      <description>GitHub Actions paired with GitHub Pages provides an excellent CD platform for a status page. Here&amp;rsquo;s how I used it to create mine.</description>
      <content:encoded><![CDATA[<p>A status page is essentially a web page that reports the health and uptime of an organization&rsquo;s various online services. <a href="https://www.githubstatus.com/">GitHub</a> has one and so does <a href="https://www.cloudflarestatus.com/">Cloudflare</a>. Most of these are powered by an <a href="https://www.atlassian.com/">Atlassian</a> product called <a href="https://www.statuspage.io/">Statuspage</a> but it&rsquo;s not always the <a href="https://www.statuspage.io/pricing?tab=public">cheapest solution</a>.</p>
<p>For hobbyist projects without any real budget (like this site and the couple others I run), Statuspage pricing is often too steep. To this effect, many open source projects exist to let you generate your own status page through an application that handles continuous updates of it. That works too! But what if you don&rsquo;t have a separate server to run the status page service on? Hosting on a server with other applications that its supposed to track is obviously not an option. Enter <a href="https://github.com/Cyclenerd/static_status">static_status</a>.</p>
<p><a href="https://github.com/Cyclenerd/static_status">static_status</a> is a bash script that as its name suggests, generates a fully static webpage that functions as a status page for the services you ask it to monitor. You can check out how it looks at <a href="https://status.msfjarvis.dev">status.msfjarvis.dev</a>. Pretty neat, right?</p>
<p><a href="https://status.msfjarvis.dev">status.msfjarvis.dev</a> is powered by a GitHub Action running every 30 minutes to deploy the generated static page to GitHub Pages and barely takes any time to set up. Here&rsquo;s how it works.</p>
<ul>
<li>First thing that you want to do is to setup the <code>CNAME</code> record that will let GitHub Pages service your status page to a subdomain of your website. Head to your domain registrar (Cloudflare for me) and add a CNAME record for <code>&lt;your github username&gt;.github.io</code></li>
</ul>
<p><img alt="CNAME record for status.msfjarvis.dev at Cloudflare" loading="lazy" src="/posts/creating-a-continuously-deploying-static-statuspage-with-github/statuspage_cname_record.webp"></p>
<ul>
<li>Next, create a GitHub repository that will hold the Actions workflow for generating your status page as well as the actual status page itself. This repo can be private, as the generated sites are always publicly available.</li>
</ul>
<p><img alt="GitHub repository for our status page" loading="lazy" src="/posts/creating-a-continuously-deploying-static-statuspage-with-github/statuspage_github_repo.webp"></p>
<ul>
<li>Clone this empty repository. Now create a file with the name of <code>CNAME</code> and enter your custom domain into it. This lets GitHub Pages know where to redirect users if they ever access the site through your <code>.github.io</code> subdomain. Commit this file.</li>
</ul>
<p><img alt="CNAME file in repository" loading="lazy" src="/posts/creating-a-continuously-deploying-static-statuspage-with-github/statuspage_cname_file.webp"></p>
<ul>
<li>
<p>A quick glance at the static_status README will inform you about the <code>config</code> file that it uses to configure itself, and status_hostname_list.txt which has a list of all services it needs to check. <code>config</code> is easy to understand and modify, so I&rsquo;ll skip it (you can diff <a href="https://github.com/msfjarvis/status.msfjarvis.dev/blob/master/config">mine</a> with upstream and use the changes to educate yourself should the need arise). This part should be very straightforward, though I did encounter a problem where using <code>ping</code> as the detection mechanism caused sites to falsely report as down. Switching to <code>curl</code> resolved the issue.</p>
</li>
<li>
<p>Finally, time to add the automation to this whole thing. Any CI solution with a cron/schedule option will work, I used GitHub Actions, you don&rsquo;t have to. I set a schedule of once every 30 minutes, depending on what platform you&rsquo;re using for CD and what services you&rsquo;re hosting, you might want to choose a shorter period. Here&rsquo;s my GitHub Actions workflow.</p>
</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Update status page&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">schedule</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">cron</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;*/30 * * * *&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">update-status-page</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Install traceroute</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">sudo apt-get install traceroute -y</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Checkout config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Checkout static_status</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">repository</span><span class="p">:</span><span class="w"> </span><span class="l">Cyclenerd/static_status</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">static_status</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">clean</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Generate status page</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          mkdir -p static_status/status/
</span></span></span><span class="line"><span class="cl"><span class="sd">          cp config static_status/
</span></span></span><span class="line"><span class="cl"><span class="sd">          cp status_hostname_list.txt static_status/
</span></span></span><span class="line"><span class="cl"><span class="sd">          cp CNAME static_status/status/
</span></span></span><span class="line"><span class="cl"><span class="sd">          cd static_status/
</span></span></span><span class="line"><span class="cl"><span class="sd">          rm status_maintenance_text.txt
</span></span></span><span class="line"><span class="cl"><span class="sd">          ./status.sh</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">peaceiris/actions-gh-pages@v2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">PERSONAL_TOKEN</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.PERSONAL_TOKEN }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">PUBLISH_BRANCH</span><span class="p">:</span><span class="w"> </span><span class="l">gh-pages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">PUBLISH_DIR</span><span class="p">:</span><span class="w"> </span><span class="l">./static_status/status</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">SCRIPT_MODE</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">username</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;MSF-Jarvis&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">useremail</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;msfjarvis+github_alt@gmail.com&#34;</span><span class="w">
</span></span></span></code></pre></div><p>This installs <code>traceroute</code> which is needed by static_status, checks out my repository, clones static_status to the static_status directory, copies over config and hostname list to that folder, places the <code>CNAME</code> file in the static_status/status directory, runs the script to generate the status page, and finally publishes the static_status/status folder to the <code>gh-pages</code> branch, as my bot account.</p>
<p>The result of this is a simple and fast statuspage that can be hosted anywhere by simply coping the single <code>index.html</code> over. If you have a separate server to run this off, you can get away with replacing this entire process with a single crontab command. Being a bash script lets static_status run on essentially any Linux-based platform so you can actually deploy this from a Raspberry Pi with no effort. Hope this helps you to create your own status pages!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Adding social metadata to your Hugo sites</title>
      <link>https://msfjarvis.dev/posts/adding-social-metadata-to-your-hugo-sites/</link>
      <pubDate>Mon, 03 Feb 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/adding-social-metadata-to-your-hugo-sites/</guid>
      <description>Optimize social media exposure with the right metadata for your site</description>
      <content:encoded><![CDATA[<p>Metadata is data (information) about data.</p>
<p>The <code>&lt;meta&gt;</code> tag provides metadata about the HTML document. Metadata will not be displayed on the page, but will be machine parsable.</p>
<p>This metadata can be used by browsers (how to display content or reload page), search engines (keywords), or other web services.</p>
<p>Here&rsquo;s how your website will look like on Twitter with and without metadata.</p>
<p><img alt="No metadata" loading="lazy" src="/posts/adding-social-metadata-to-your-hugo-sites/hugo_metadata_no_meta.webp"></p>
<p><img alt="Correct metadata" loading="lazy" src="/posts/adding-social-metadata-to-your-hugo-sites/hugo_metadata_correct_meta.webp"></p>
<p>You be the judge of what you like better :)</p>
<h2 id="automatically-adding-social-metadata-to-hugo-sites">Automatically adding social metadata to Hugo sites</h2>
<p>After coming across <a href="https://github.com/budparr/awesome-hugo#theme-components">this list</a> I realized theme components was a thing so I&rsquo;ve extracted my <a href="https://github.com/msfjarvis/msfjarvis.dev/commit/cc08039a6b4a6b649bdd8710295383d2388c9955">social metadata commit</a> into a separate component for re-use by the community. It&rsquo;s available on GitHub at <a href="https://github.com/msfjarvis/hugo-social-metadata">msfjarvis/hugo-social-metadata</a>. The README goes through the installation steps so here I will simply cover what the component is actually adding. Here&rsquo;s the generated metadata for this very post.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">meta</span> <span class="na">property</span><span class="o">=</span><span class="s">&#34;og:type&#34;</span> <span class="na">content</span><span class="o">=</span><span class="s">&#34;website&#34;</span> <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">meta</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;twitter:card&#34;</span> <span class="na">content</span><span class="o">=</span><span class="s">&#34;summary_large_image&#34;</span> <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">meta</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;twitter:site&#34;</span> <span class="na">content</span><span class="o">=</span><span class="s">&#34;@msfjarvis&#34;</span> <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">meta</span>
</span></span><span class="line"><span class="cl">  <span class="na">name</span><span class="o">=</span><span class="s">&#34;description&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="na">content</span><span class="o">=</span><span class="s">&#34;Optimize social media exposure with the right metadata for your site&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">meta</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;keywords&#34;</span> <span class="na">content</span><span class="o">=</span><span class="s">&#34;hugo,webdev,static sites,&#34;</span> <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">meta</span>
</span></span><span class="line"><span class="cl">  <span class="na">property</span><span class="o">=</span><span class="s">&#34;og:url&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="na">content</span><span class="o">=</span><span class="s">&#34;https://msfjarvis.dev/posts/adding-social-metadata-to-your-hugo-sites/&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">meta</span>
</span></span><span class="line"><span class="cl">  <span class="na">property</span><span class="o">=</span><span class="s">&#34;og:title&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="na">content</span><span class="o">=</span><span class="s">&#34;Adding social metadata to your Hugo sites &amp;middot; Harsh Shandilya&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">meta</span>
</span></span><span class="line"><span class="cl">  <span class="na">name</span><span class="o">=</span><span class="s">&#34;twitter:title&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="na">content</span><span class="o">=</span><span class="s">&#34;Adding social metadata to your Hugo sites &amp;middot; Harsh Shandilya&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">meta</span>
</span></span><span class="line"><span class="cl">  <span class="na">name</span><span class="o">=</span><span class="s">&#34;og:description&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="na">content</span><span class="o">=</span><span class="s">&#34;Optimize social media exposure with the right metadata for your site&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">meta</span>
</span></span><span class="line"><span class="cl">  <span class="na">name</span><span class="o">=</span><span class="s">&#34;twitter:description&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="na">content</span><span class="o">=</span><span class="s">&#34;Optimize social media exposure with the right metadata for your site&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">meta</span>
</span></span><span class="line"><span class="cl">  <span class="na">name</span><span class="o">=</span><span class="s">&#34;twitter:url&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="na">content</span><span class="o">=</span><span class="s">&#34;https://msfjarvis.dev/posts/adding-social-metadata-to-your-hugo-sites/&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">meta</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;twitter:image:src&#34;</span> <span class="na">content</span><span class="o">=</span><span class="s">&#34;android-chrome-512x512.webp&#34;</span> <span class="p">/&gt;</span>
</span></span></code></pre></div><ul>
<li><code>og:type</code> - Allowed values are specified at the OpenGraph protocol&rsquo;s documentation <a href="https://ogp.me/#types">here</a>. I use <code>website</code> to reflect the content I serve.</li>
<li><code>twitter:card</code> - One of <code>summary</code>, <code>summary_large_image</code>, <code>app</code>, or <code>player</code>. <code>summary_large_image</code> indicates that I want to see a social image as well as the description I provide when this is rendered on Twitter.</li>
<li><code>twitter:site</code> - Twitter username of the owner of this website.</li>
<li><code>description</code> - HTML5 tag that describes the content of this page. The content of this can be replicated in <code>og:description</code> and <code>twitter:description</code> to satisfy Facebook and Twitter respectively.</li>
<li><code>og:url</code> and <code>twitter:url</code> - Permalink to the content that this page is for. You can use this to provide a link with tracking related metadata to track social origins.</li>
<li><code>og:title</code> and <code>twitter:title</code> - Title of the page as you want it to be shown on social media.</li>
<li><code>twitter:image:src</code> - Absolute link to an image that will be used in your Twitter card.</li>
</ul>
]]></content:encoded>
    </item>
    
    <item>
      <title>Dagger the easy way - Part 1</title>
      <link>https://msfjarvis.dev/posts/dagger-the-easy-way--part-1/</link>
      <pubDate>Mon, 20 Jan 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/dagger-the-easy-way--part-1/</guid>
      <description>Dagger is universally intimidating to beginners and I want to change it.</description>
      <content:encoded><![CDATA[<blockquote>
<p>Updated on 22 Jan 2020 with some additional comments from <a href="https://twitter.com/arunkumar_9t2">@arunkumar_9t2</a>. Look out for them as block quotes similar to this one.</p>
</blockquote>
<p>This is not your average coding tutorial. I&rsquo;m going to show you how to write actual Dagger code and skip all the scary and off-putting parts about the implementation details of the things we&rsquo;re using and how Dagger does everything under the hood.</p>
<p>With that out of the way, onwards to the actual content. We&rsquo;re going to be building a very simple app that does just one thing, show a Toast with some text depending on whether it was the first run or not. Nothing super fancy here, but with some overkill abstraction I&rsquo;ll hopefully be able to demonstrate a straightforward and understandable use of Dagger.</p>
<p>I&rsquo;ve setup a repository at <a href="https://github.com/msfjarvis/dagger-the-easy-way">msfjarvis/dagger-the-easy-way</a> that shows every logical collection of changes in its own separate commit, and also a PR to go from no DI to Dagger so you can browse changes in bulk as well.</p>
<h2 id="the-mandatory-theory">The mandatory theory</h2>
<p>I know what I said, but this is just necessary. Bear with me.</p>
<h3 id="component"><code>Component</code></h3>
<p>A Component defines an interface that Dagger constructs to know the entry points where dependencies can be injected. It can also hold the component factory that instructs Dagger how to construct said component. A Component <em>also</em> holds the list of modules.</p>
<h3 id="module"><code>Module</code></h3>
<p>A Module is any logical unit that contributes to Dagger&rsquo;s object graph. In simpler terms, any <code>class</code> or <code>object</code> that has declarations which tell Dagger how to construct a particular dependency, is annotated with <code>@Module</code>.</p>
<blockquote>
<p><em>Arun&rsquo;s notes</em></p>
<p>Modules should be mentioned first here, as they&rsquo;re the smallest units of a Dagger setup, and Components build upon them. An alternate definition for a module can also be this: if we draw a graph, methods in @Module classes become the nodes and @Component is the holder of those nodes.</p>
</blockquote>
<h2 id="getting-started">Getting Started</h2>
<p>To get started, clone the repository which contains all the useless grunt work already done for you. Use <code>git clone https://github.com/msfjarvis/dagger-the-easy-way</code> if you&rsquo;re unfamiliar with branch selection during clone.</p>
<p>The repo in this stage is very bare - it has the usual boilerplate and just one class, <code>MainActivity</code>. We&rsquo;re going to make this a bit more interesting shortly.</p>
<p>Switch to the <code>part-1</code> branch, which has a bit more in terms of commit history and code. This is what we&rsquo;re going to work with.</p>
<h2 id="setting-up-the-object-graph">Setting up the object graph</h2>
<p>Remember <code>Component</code> and <code>Module</code>? It&rsquo;s gonna come in handy here.</p>
<p>Start off with <a href="https://github.com/msfjarvis/dagger-the-easy-way/commit/f86208b89cee2c05becd4341e1b209dc2479aa2f">adding the Dagger dependencies</a>, then add an <strong>empty</strong> Component and Module, which we did <a href="https://github.com/msfjarvis/dagger-the-easy-way/commit/f1604adb4e99f342b213cefa9fada21efb6f49a2">here</a>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="nd">@Singleton</span>
</span></span><span class="line"><span class="cl"><span class="nd">@Component</span><span class="p">(</span><span class="n">modules</span> <span class="p">=</span> <span class="p">[</span><span class="n">AppModule</span><span class="o">::</span><span class="k">class</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="k">interface</span> <span class="nc">AppComponent</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@Module</span>
</span></span><span class="line"><span class="cl"><span class="k">object</span> <span class="nc">AppModule</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>What we&rsquo;re doing here, is marking our <code>AppComponent</code> as a &lsquo;singleton&rsquo;, to indicate that it needs to only be constructed <em>once</em> for the lifecycle of the application. We&rsquo;re also annotating it with <code>@Component</code> for obvious reasons, and adding our module to it to indicate that they&rsquo;re going together. This is empty right now but that&rsquo;s going to change soon.</p>
<blockquote>
<p><em>Arun&rsquo;s notes</em></p>
<p>Annotating with @Singleton is only effective when configured properly. For this to be a singleton, you need to ensure you&rsquo;re creating this only once and that&rsquo;s your responsibility to fulfill. This is part of scoping and is a great topic to be covered in part 2.</p>
</blockquote>
<p>If you check <code>MainActivity</code>, you&rsquo;ll notice that we&rsquo;re using <a href="https://developer.android.com/reference/android/content/SharedPreferences.html">SharedPreferences</a>. To demonstrate the use of Dagger, I&rsquo;m going to replace that usage with one provided through Dagger. For that to happen though, Dagger needs to know how to create a <code>SharedPreferences</code>. Let&rsquo;s get that going!</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="nd">@Module</span>
</span></span><span class="line"><span class="cl"><span class="k">object</span> <span class="nc">AppModule</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nd">@Provides</span>
</span></span><span class="line"><span class="cl">    <span class="nd">@Reusable</span>
</span></span><span class="line"><span class="cl">    <span class="k">fun</span> <span class="nf">provideSharedPrefs</span><span class="p">(</span><span class="n">context</span><span class="p">:</span> <span class="n">Context</span><span class="p">):</span> <span class="n">SharedPreferences</span> <span class="p">=</span> <span class="nc">PreferenceManager</span><span class="p">.</span><span class="n">getDefaultSharedPreferences</span><span class="p">(</span><span class="n">context</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Breaking this down: <code>Provides</code> tells Dagger to bind the return value of the method to the object graph, and <code>Reusable</code> tells Dagger that you want to use one copy of this as many times as you can, but it&rsquo;s <em>okay</em> to create a new instance if that&rsquo;s not possible.</p>
<p>If you pay attention to the <a href="https://github.com/msfjarvis/dagger-the-easy-way/commit/f1a60ffaf6f07f8654bde27fbd65bef08c248f4e">commit</a> for this step, you&rsquo;ll see that we&rsquo;re also adding preferences to the <code>AppComponent</code>. This is just one of the many different patterns one can use with Dagger, and I&rsquo;m using it just for the simplicity. We&rsquo;ll look into another way of doing this for the next part.</p>
<h2 id="initializing-our-component">Initializing our component</h2>
<p>Now for Dagger to know when to create this graph, it needs to be able to know how to initialize the <code>Component</code> we wrote earlier. For this, we&rsquo;ll be adding a factory that constructs the <code>AppComponent</code>. Since we need a Context to be able to create <code>SharedPreferences</code>, we&rsquo;ll make our factory accept a context parameter.</p>
<blockquote>
<p><em>Arun&rsquo;s notes</em></p>
<p>Worth nothing that the reason we create a factory method accepting Context instead of letting Dagger provide is because we don&rsquo;t have hold of Context during compile time. The instance is created by Android system and given to us which we then use Factory to give it to dagger.</p>
</blockquote>
<p>Here&rsquo;s how the finished <code>AppComponent</code> looks like with the factory method.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="nd">@Singleton</span>
</span></span><span class="line"><span class="cl"><span class="nd">@Component</span><span class="p">(</span><span class="n">modules</span> <span class="p">=</span> <span class="p">[</span><span class="n">AppModule</span><span class="o">::</span><span class="k">class</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="k">interface</span> <span class="nc">AppComponent</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nd">@Component</span><span class="p">.</span><span class="n">Factory</span>
</span></span><span class="line"><span class="cl">    <span class="k">interface</span> <span class="nc">Factory</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">fun</span> <span class="nf">create</span><span class="p">(</span><span class="nd">@BindsInstance</span> <span class="n">applicationContext</span><span class="p">:</span> <span class="n">Context</span><span class="p">):</span> <span class="n">AppComponent</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">val</span> <span class="py">preferences</span><span class="p">:</span> <span class="n">SharedPreferences</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>The <code>BindsInstance</code> annotation tells Dagger that we&rsquo;ll be providing our own Context and that it does not have to know how to create one.</p>
<p>As the parameter name suggests, we&rsquo;ll be using an application-scoped Context for this, so let&rsquo;s initialize the Component in an Application class. We&rsquo;ll be accessing our dependencies through this initialized component, and the Application class is always initialized first so that let&rsquo;s us avoid any situation where we try to refer to the component and find that it&rsquo;s null.</p>
<p>Create an Application class, make it extend <code>android.app.Application</code>, and add it to the manifest ( <a href="https://github.com/msfjarvis/dagger-the-easy-way/commit/25d4dc223bfafd40ac9801e23ca9b09526ed9362">Reference commit</a>).</p>
<p>Now we&rsquo;ll be adding our component here. Since we&rsquo;ll be accessing it from other classes, we&rsquo;ll make it static. The Application class lives as long as our process does, so we&rsquo;re safe from a life-cycle perspective. Here&rsquo;s the finished <code>ExampleApplication</code> class.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">ExampleApplication</span> <span class="p">:</span> <span class="n">Application</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">override</span> <span class="k">fun</span> <span class="nf">onCreate</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">super</span><span class="p">.</span><span class="n">onCreate</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">component</span> <span class="p">=</span> <span class="nc">DaggerAppComponent</span><span class="p">.</span><span class="n">factory</span><span class="p">().</span><span class="n">create</span><span class="p">(</span><span class="k">this</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">companion</span> <span class="k">object</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">lateinit</span> <span class="k">var</span> <span class="py">component</span><span class="p">:</span> <span class="n">AppComponent</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><blockquote>
<p><em>Arun&rsquo;s notes</em></p>
<p>Nit: I like to do something like <a href="https://github.com/arunkumar9t2/scabbard/blob/004116cf6a548022982c7869d7758725c18991f8/scabbard-sample/src/main/java/dev/arunkumar/scabbard/App.kt#L10">this</a>. The reason is, since it is a val, it will not be editable and also lazy being lazy means it will cached.</p>
</blockquote>
<p>Notice the <code>DaggerAppComponent</code> class that did not exist before. This is a Dagger generated version of our <code>AppComponent</code> interface that is suitable for instantiation. This class holds the factory method we created before, and returns an instance of <code>AppComponent</code> that let&rsquo;s us access the dependencies we installed into the component. When we initialize our component, Dagger also intelligently creates all the dependencies in our graph. Now all that&rsquo;s left for us is to use the dependencies we declared in our app.</p>
<h2 id="injecting-dependencies">Injecting dependencies</h2>
<p>Head on over to <code>MainActivity</code> now. Notice that we initialize a <code>SharedPreferences</code> object there, which can be replaced with the one we asked Dagger to create for us. Let&rsquo;s do that!</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"> class MainActivity : AppCompatActivity() {
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gi">+    private val prefs = ExampleApplication.component.preferences
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl">     override fun onCreate(savedInstanceState: Bundle?) {
</span></span><span class="line"><span class="cl">         super.onCreate(savedInstanceState)
</span></span><span class="line"><span class="cl">         setContentView(R.layout.activity_main)
</span></span><span class="line"><span class="cl"><span class="gd">-        val prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
</span></span></span><span class="line"><span class="cl">         if (prefs.getBoolean(&#34;first_start&#34;, true)) {
</span></span><span class="line"><span class="cl">             Toast.makeText(this, &#34;First start!&#34;, Toast.LENGTH_LONG).show()
</span></span><span class="line"><span class="cl">             prefs.edit().putBoolean(&#34;first_start&#34;, false).apply()
</span></span></code></pre></div><p>And that&rsquo;s it. Really. Now you&rsquo;re using Dagger to provide a dependency. It&rsquo;s that simple!</p>
<h2 id="conclusion">Conclusion</h2>
<p>As you&rsquo;ve seen here, using Dagger does not always have to involve complexity. Dagger can be used in projects of any size, of any complexity, and in any fashion that you deem fit. The example above is a very simple use of Dagger, and has scope for further improvement which we&rsquo;ll be looking into.</p>
<p>This is my first time writing about using Dagger, having only <a href="/posts/my-dagger-story/">recently started using and liking it</a>. Please let me know about any parts that were too complex, factually incorrect or just lacking in any way, and I will be more than glad to improve this.</p>
<p>In the next part, we&rsquo;ll be looking into constructor injection, why it&rsquo;s generally better, and how to inject dependencies into classes that we don&rsquo;t own (like activities and fragments) with the help of the <code>@Inject</code> annotation. Thanks for reading this far!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Integrating comments in Hugo sites with commento</title>
      <link>https://msfjarvis.dev/posts/integrating-comments-in-hugo-sites-with-commento/</link>
      <pubDate>Mon, 20 Jan 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/integrating-comments-in-hugo-sites-with-commento/</guid>
      <description>Adding additional comment backends to Hugo is actually rather simple!</description>
      <content:encoded><![CDATA[<p>Disqus is unequivocally the leader when it comes to hosted comments, and it works rather swimmingly with sites of all kinds with minimal hassle. But this ease has a gnarly flipside: <a href="https://stiobhart.net/2017-02-21-disqusting/">annoying referral links</a> and a <a href="https://victorzhou.com/blog/replacing-disqus/">huge bundle size</a> that significantly affects page load speeds.</p>
<p>As I was considering adding comments to this blog, I went through these posts and realised that Disqus is not going to be satisfactory enough, especially after the time and effort I put into improving bundle sizes and page loading. I started looking into the alternatives, and shortlisted <a href="https://posativ.org/isso">Isso</a> and <a href="https://commento.io/">Commento</a>. Going through Isso documentation and <a href="https://stiobhart.net/2017-02-24-isso-comments/">this post</a> it was clear that setup was going to be a bit of a chore, and that was the end of it.</p>
<p>Commento is open source just like Isso, but has a cloud-hosted option. I was interested in self-hosting, though, and I was glad to find that Commento delivered very well on that front too. <a href="https://docs.commento.io/installation/self-hosting/on-your-server/docker.html#with-docker-compose">docker-compose</a> is an officially supported deployment method and I was pleased to see that setup went forward without a problem.</p>
<h2 id="integrating-with-hugo">Integrating with Hugo</h2>
<p>The interesting part! Hugo offers a Disqus template internally, but any other comment system&rsquo;s going to need some legwork done. Commento&rsquo;s integration code is just two lines, as you can see below.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;commento&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">defer</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;https://commento.example.com/js/commento.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>Hugo offers a powerful tool called <a href="https://gohugo.io/templates/partials/#use-partials-in-your-templates">partials</a> that allows injecting code into pages from another HTML file. I quickly created a partial with the integration code, scoped out the domain with a variable, and ended up with this.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;commento&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">defer</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;{{ .Site.Params.CommentoURL }}/js/commento.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">noscript</span><span class="p">&gt;</span>Please enable JavaScript to load the comments.<span class="p">&lt;/</span><span class="nt">noscript</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>With this saved as <code>layouts/partials/commento.html</code> and <code>CommentoURL</code> set in my <code>config.toml</code>, I set out to wire this into the posts. Because of a <a href="https://github.com/msfjarvis/msfjarvis.dev/commit/5447bb36258934d6a5bc86be99ef91a9eeb9eb17">pre-existing hack</a> that I use for linkifying headings, I already had the <code>single.html</code> file from my theme copied into <code>layouts/_default/single.html</code>. If you don&rsquo;t, copy it over and open it. Add the following lines, removing any mention of Disqus if you find it.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nx">and</span><span class="w"> </span><span class="p">.</span><span class="nx">Site</span><span class="p">.</span><span class="nx">Params</span><span class="p">.</span><span class="nf">CommentoURL</span><span class="w"> </span><span class="p">(</span><span class="nf">and</span><span class="w"> </span><span class="p">(</span><span class="nx">not</span><span class="w"> </span><span class="p">.</span><span class="nx">Site</span><span class="p">.</span><span class="nx">BuildDrafts</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nx">not</span><span class="w"> </span><span class="nx">hugo</span><span class="p">.</span><span class="nx">IsServer</span><span class="p">))</span><span class="w"> </span><span class="o">-</span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nx">h2</span><span class="p">&gt;</span><span class="nx">Comments</span><span class="p">&lt;</span><span class="o">/</span><span class="nx">h2</span><span class="p">&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">partial</span><span class="w"> </span><span class="s">&#34;commento.html&#34;</span><span class="w"> </span><span class="p">.</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="o">-</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span></code></pre></div><p>With this, the comments section is only loaded when CommentoURL is defined, and the site is not running in server mode. This allows me to exclude showing comments when using the preview server on <a href="https://forestry.io">Forestry</a> (highly recommended CMS for Hugo, by far my personal favorite). Since I also have a copy of my site with drafts enabled hosted on a separate subdomain, I had to factor that into the partial as well. Here&rsquo;s what I deploy on my own website.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nx">and</span><span class="w"> </span><span class="p">.</span><span class="nx">Site</span><span class="p">.</span><span class="nx">Params</span><span class="p">.</span><span class="nf">CommentoURL</span><span class="w"> </span><span class="p">(</span><span class="nf">and</span><span class="w"> </span><span class="p">(</span><span class="nx">not</span><span class="w"> </span><span class="p">.</span><span class="nx">Site</span><span class="p">.</span><span class="nx">BuildDrafts</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nx">not</span><span class="w"> </span><span class="nx">hugo</span><span class="p">.</span><span class="nx">IsServer</span><span class="p">))</span><span class="w"> </span><span class="o">-</span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">&lt;!</span><span class="o">--</span><span class="w"> </span><span class="nx">Rest</span><span class="w"> </span><span class="nx">is</span><span class="w"> </span><span class="nx">identical</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">the</span><span class="w"> </span><span class="nx">previous</span><span class="w"> </span><span class="o">--</span><span class="p">&gt;</span><span class="w">
</span></span></span></code></pre></div><p>And that&rsquo;s it! Now you should have a fully functioning comment system on your static sites that does not bloat the bundle size unnecessarily.</p>
<p>P.S. If anybody&rsquo;s interested to have me cover the template language for Hugo (conditionals, loops and the like), put it down in the comments :P</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>My Dagger Story</title>
      <link>https://msfjarvis.dev/posts/my-dagger-story/</link>
      <pubDate>Sat, 11 Jan 2020 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/my-dagger-story/</guid>
      <description>Dagger is not the easiest tool to get on board with but it&amp;rsquo;s almost worth the effort. Here&amp;rsquo;s the story of my journey to not hating Dagger.</description>
      <content:encoded><![CDATA[<p><a href="https://dagger.dev">Dagger</a> is infamous for very good reasons. It&rsquo;s complicated to use, the documentation is an absolute shitshow, and simpler &lsquo;alternatives&rsquo; exist. While <a href="http://insert-koin.io/">Koin</a> and to a lesser extent <a href="https://kodein.org/di/">Kodein</a> do the job, they&rsquo;re still service locators at their core and don&rsquo;t automatically inject dependencies like Dagger does.</p>
<h2 id="background">Background</h2>
<p>Before I start, some introductions. I&rsquo;m the sole developer of <a href="https://play.google.com/store/apps/details?id=me.msfjarvis.viscerion">Viscerion</a>, an Android client for <a href="https://www.wireguard.com/">WireGuard</a>. It is a fully <a href="https://github.com/msfjarvis/viscerion">open source</a> project just like the upstream Android client which served as the base for it. I forked Viscerion as a playground project that&rsquo;d let me learn new Android tech in an unencumbered way and be something that I&rsquo;d actually use, hence guaranteeing some level of sustained interest. I do still contribute major fixes back upstream, so put down your pitchforks :P</p>
<p>Like I said before, Viscerion is a learning endeavour, so I decided to learn. I rewrote most of the app in Kotlin, implemented some UI changes to make the app more friendlier to humans and added a couple features here and there.</p>
<p>And then I decided to tackle the Dependency Injection monster.</p>
<h2 id="the-beginnings">The beginnings</h2>
<p>The dependency injection story always begins with the search for a library that does it for you. You look at Dagger, go to the documentation, look up what a Thermosiphon is, then scratch Dagger off the list and move on. Kotlin users will then end up on one of <a href="http://insert-koin.io/">Koin</a> or <a href="https://kodein.org/di/">Kodein</a> and that&rsquo;ll be the end of their story.</p>
<p>That was mine as well! Before Viscerion was forked, the app used to have Dagger (albeit sorely underused as I can tell now that I know a fair bit about it) but then it was <a href="https://github.com/WireGuard/wireguard-android/commit/712b6c6f600ef6eb683d356a6e9a05e9415b7e12">swapped out</a> for singleton access from the Application class. I really, really wanted to try out the fancy &lsquo;Dependency Injection&rsquo; thing everybody loved so I did some searching around, went through the aforementioned motions and <a href="https://github.com/msfjarvis/viscerion/pull/131">settled on Koin</a>.</p>
<p>It was great! I could get any dependency anywhere and that allowed me to write all kinds of hot garbage, and garbage I wrote. But despite all that, I still really wanted to give Dagger another shot. I tried multiple times to make it work, the remains of which have been force pushed away long since. Then I came across <a href="https://twitter.com/tfcporciuncula">Fred Porciúncula</a>&rsquo;s <a href="https://github.com/tfcporciuncula/dagger-journey">dagger-journey</a> repository and the accompanying talk that tried to fill the usability gap that Dagger&rsquo;s documentation never could. He was largely successful in being able to teach me how to use Dagger, and the first &ldquo;proper&rdquo; attempt I made at using Dagger was <a href="https://github.com/msfjarvis/viscerion/pull/196/files">largely decent</a>. I was still missing a lot of knowledge that made me <a href="https://github.com/msfjarvis/viscerion/pull/196#issuecomment-557907972">slip back into my hate-train</a>.</p>
<h2 id="the-turning-point">The turning point</h2>
<p>Around mid-December 2019, <a href="https://twitter.com/arunkumar_9t2">Arun</a> released the 0.1 version of his Dagger 2 dependency graph visualizer, <a href="https://arunkumar.dev/introducing-scabbard-a-tool-to-visualize-dagger-2-dependency-graphs/">Scabbard</a>. It looked <strong>awesome</strong>. I reshared it and shoved in my residual Dagger hate for good measure because isn&rsquo;t that what the internet is for. I was confident that Dagger shall never find a place in my code and my friend <a href="https://sasikanth.dev">Sasikanth</a> was hell-bent on ensuring otherwise.</p>
<p>Together, we dug up my previous efforts and I started <a href="https://github.com/msfjarvis/viscerion/pull/214">a PR</a> so he could review it and help me past the point I dropped out last time. He helped <a href="https://github.com/msfjarvis/viscerion/pull/214#pullrequestreview-336919368">me on GitHub</a>, privately on Telegram and together in about 2 days Viscerion was completely Koin-free and ready to kill. I put down my thoughts about the migration briefly <a href="https://github.com/msfjarvis/viscerion/pull/214#issuecomment-569541678">on the PR</a>, which I&rsquo;ll reproduce and expand on below.</p>
<blockquote>
<ul>
<li>Dagger is ridiculously complex without a human to guide you around.</li>
</ul>
</blockquote>
<p>I will die on this hill. Without Sasikanth&rsquo;s help I would have never gotten around to even <em>trying</em> Dagger again.</p>
<blockquote>
<ul>
<li>Koin&rsquo;s service locator pattern makes it far too easy to write bad code because you can inject anything anywhere.</li>
</ul>
</blockquote>
<p>Again, very strong opinion that I will continue to have. I overlooked a clean way of implementing a feature and went for a quick and dirty version because Koin allowed me the freedom to do it. Dagger forced me to re-evaluate my code and I ended up being able to extract all Android dependencies from that package and move it into a separate module.</p>
<blockquote>
<ul>
<li>Dagger can feel like a lot of boilerplate but some clever techniques can mitigate that.</li>
</ul>
</blockquote>
<p>Because the Dagger documentation wasn&rsquo;t helpful, I didn&rsquo;t realise that a <code>Provides</code> annotated method and an <code>@Inject</code>ed constructor was an either-or situation and I didn&rsquo;t need to write both for a class to be injectable. Sasikanth <a href="https://github.com/msfjarvis/viscerion/pull/214#discussion_r361800427">with the rescue again</a>.</p>
<blockquote>
<ul>
<li>Writing <code>inject</code> methods for every single class can feel like a drag because it is.</li>
</ul>
</blockquote>
<p><a href="https://github.com/msfjarvis/viscerion/blob/4a40f3692e62939d3b4c3693efe41ad03fb5f330/app/src/main/java/com/wireguard/android/di/AppComponent.kt#L69-L101">I mean...</a></p>
<blockquote>
<ul>
<li>Injecting into Kotlin <code>object</code>s appears to be a no-go. I opted to <a href="https://github.com/msfjarvis/viscerion/pull/214/commits/9eb532521f51d0f7bb66a2a78aa1fc5688128a22">refactor out the staticity where possible</a>, <a href="https://github.com/msfjarvis/viscerion/commit/e23f878140d4bda9e2c54d6c2684e07994066fd6#diff-28007a5799b03e7b556f5bb942754031">pass injected dependencies to the function</a> or <a href="https://github.com/msfjarvis/viscerion/pull/214/commits/fc54ec6bb8e99ec639c6617765e814e12d91ea1a#diff-74f75ab44e1cd2909c4ec4d704bbbab7R65">fall back to 'dirty' patterns</a> as needed. Do what you feel like.</li>
</ul>
</blockquote>
<p>I have no idea if that&rsquo;s even a good ability to begin with, so I chose to change myself rather than fight the system.</p>
<blockquote>
<ul>
<li>I still do not <em>love</em> Dagger. Fuck you Google.</li>
</ul>
</blockquote>
<p>This, I probably don&rsquo;t subscribe to anymore. Dagger was horrible to get started with, but I can now claim passing knowledge and familiarity with it, enough to be able to use it for simple projects and be comfortable while doing so.</p>
<h2 id="to-summarize">To summarize</h2>
<p>Like RxJava, Dagger has become an industry standard of sorts and a required skill at a lot of Android positions, so eventually you might wind up needing to learn it anyway, so why wait? Dagger is not <em>terrible</em>, just badly presented. Learning from existing code is always helpful, and that was part of how I learned. Use my PR, and post questions below and I&rsquo;ll do my best to help you like I was helped and hopefully we&rsquo;ll both learn something new :)</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Server-side analytics with Goaccess</title>
      <link>https://msfjarvis.dev/posts/serverside-stats-with-goaccess/</link>
      <pubDate>Tue, 17 Dec 2019 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/serverside-stats-with-goaccess/</guid>
      <description>Analytics platforms are often overwhelming and a privacy nightmare &amp;ndash; here&amp;rsquo;s how to bring analytics to the backend with very simple tooling</description>
      <content:encoded><![CDATA[<p>Analytics are a very helpful aspect of any development. They allow developers to know what parts of their apps are visited the most often and can use more attention, and for bloggers to know what content does or does not resonate with their readers.</p>
<p>There are many, many analytics providers and software stacks each with their specific pros and cons, but nearly all managed analytics come with the overarching concern of privacy of user data. <a href="https://analytics.google.com/">Google Analytics</a> is a <em>huge</em> analytics vendor, with the capabilities to almost accurately extrapolate even the <strong>age</strong> of your visitors. That&rsquo;s nuts, and honestly scary.</p>
<p>Analytics platforms often drown us in data and statistics most of which we don&rsquo;t really care for or use. Wouldn&rsquo;t it be far easier if we were able to both remove the client-side aspect of analytics, as well as remove unused information and focus on what we need? Enter <a href="https://goaccess.io">Goaccess</a>.</p>
<h2 id="what-is-goaccess">What is Goaccess</h2>
<p>Goaccess is an <strong>open-source</strong>, <strong>real-time</strong> web log analyzer. In other words, it parses your webserver&rsquo;s logs and generates actionable reports from them in HTML, JSON or CSV, based on your needs. It is highly configurable and allows you to also modify the generated report by anonymizing IPs, ignoring crawlers, determining the real operating systems of the users and some more.</p>
<h2 id="the-setup">The setup</h2>
<p>To create a compelling analytics experience, we&rsquo;ll need to use Goaccess&rsquo; <code>--real-time-html</code> option, that creates an HTML report, and an accompanying <code>WebSocket</code> server that will dispatch a request to update the page data every time goaccess parses updated logs. Here&rsquo;s a peek at Goaccess&rsquo; terminal visualizer, to get an idea about the datasets you can expect from the web version.</p>
<p><img alt="Goaccess in the terminal" loading="lazy" src="/posts/serverside-stats-with-goaccess/goaccess_terminal.webp"></p>
<p>Goaccess supports most common webserver log formats, and <a href="https://goaccess.io/man#options">some more</a> with the option to provide your own format if you&rsquo;re using custom solutions. I&rsquo;m using <code>VCOMMON</code>, as that is the default log format of my webserver of choice, <a href="https://caddyserver.com">Caddy</a>. Here&rsquo;s the command executed by the systemd unit that I use for goaccess. I&rsquo;ll explain every option in a bit.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">goaccess --log-format<span class="o">=</span>VCOMMON <span class="se">\
</span></span></span><span class="line"><span class="cl">         --ws-url<span class="o">=</span>wss://stats.example.com/ws <span class="se">\
</span></span></span><span class="line"><span class="cl">         --output<span class="o">=</span><span class="si">${</span><span class="nv">STATS_DIR</span><span class="si">}</span>/index.html <span class="se">\
</span></span></span><span class="line"><span class="cl">         --log-file<span class="o">=</span>/etc/logs/requests.log <span class="se">\
</span></span></span><span class="line"><span class="cl">         --no-query-string <span class="se">\
</span></span></span><span class="line"><span class="cl">         --anonymize-ip <span class="se">\
</span></span></span><span class="line"><span class="cl">         --double-decode <span class="se">\
</span></span></span><span class="line"><span class="cl">         --real-os <span class="se">\
</span></span></span><span class="line"><span class="cl">         --real-time-html
</span></span></code></pre></div><ul>
<li><code>--ws-url</code>: This option allows us to specify the path for our WebSocket server that&rsquo;s responsible for dispatching updates.</li>
<li><code>--output</code>: File to dump HTML reports into.</li>
<li><code>--log-file</code>: The source file to read logs from.</li>
<li><code>--no-query-string</code>: Does not parse the query string from URLs (<code>example.org/contact?utm_source=twitter</code> =&gt; <code>example.org/contact</code>). This can greatly decrease memory consumption and is often not helpful.</li>
<li><code>--double-decode</code>: Attempts to decode values like user-agent, request and referrer that are often encoded twice.</li>
<li><code>--real-os</code>: Displays the real OS names behind the browsers.</li>
<li><code>--real-time-html</code>: The hero of the show &ndash; the option that makes our analytics real-time and self-updating in the browser.</li>
</ul>
<p>The final step in this process is to expose the local WebSocket server to the <code>/ws</code> endpoint of your domain to allow real-time updates to work. Here&rsquo;s how I do it in Caddy.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">https://stats.example.com/ws <span class="o">{</span>
</span></span><span class="line"><span class="cl">    proxy / localhost:7890 <span class="o">{</span>
</span></span><span class="line"><span class="cl">       websocket
</span></span><span class="line"><span class="cl">    <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><p>And that&rsquo;s it! Your analytics page should be up at your specified URL, updating on every new request and visitor.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>#TeachingKotlin Part 3 - Caveats coming from Java</title>
      <link>https://msfjarvis.dev/posts/teachingkotlin-part-3--caveats-coming-from-java/</link>
      <pubDate>Mon, 16 Dec 2019 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/teachingkotlin-part-3--caveats-coming-from-java/</guid>
      <description>Part 3 of #TeachingKotlin covers some subtle differences between Kotlin and Java that might affect your codebases as you start migrating to or writing new code in Kotlin.</description>
      <content:encoded><![CDATA[<p>When you start migrating your Java code to Kotlin, you will encounter multiple subtle changes that might catch you off guard. I&rsquo;ll document some of these gotchas that I and other people I follow have found and written about.</p>
<h2 id="splitting-strings">Splitting strings</h2>
<p>Java&rsquo;s <code>java.lang.String#split</code> <a href="https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#split-java.lang.String-">method</a> takes a <code>String</code> as it&rsquo;s first argument and creates a <code>Regex</code> out of it before attempting to split. Kotlin, however, has two variants of this method. One takes a <code>String</code> and uses it as a plaintext delimiter, and the other takes a <code>Regex</code> behaving like the Java method we mentioned earlier. Code that was directly converted from Java to Kotlin will fail to accommodate this difference, so be on the lookout.</p>
<h2 id="runtime-asserts">Runtime asserts</h2>
<p>Square&rsquo;s <a href="https://twitter.com/jessewilson">Jesse Wilson</a> found through an <a href="https://github.com/square/okhttp/issues/5586">OkHttp bug</a> that Kotlin&rsquo;s <code>assert</code> function differs from Java&rsquo;s in a very critical way - the asserted expression is <em>always</em> executed. He&rsquo;s written about it on his blog which you can check out for a proper write up: <a href="https://publicobject.com/2019/11/18/kotlins-assert-is-not-like-javas-assert/">Kotlin’s Assert Is Not Like Java’s Assert</a>.</p>
<p>TL; DR Java&rsquo;s <code>assert</code> checks the <code>java.lang.Class#desiredAssertionStatus</code> method <strong>before</strong> executing the expression, but Kotlin does it <strong>after</strong> which results in unnecessary, potentially significant overhead.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="c1">// Good :)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nd">@Override</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">flush</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">Http2Stream</span><span class="p">.</span><span class="na">class</span><span class="p">.</span><span class="na">desiredAssertionStatus</span><span class="p">())</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="n">Thread</span><span class="p">.</span><span class="na">holdsLock</span><span class="p">(</span><span class="n">Http2Stream</span><span class="p">.</span><span class="na">this</span><span class="p">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="kc">false</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="k">throw</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">AssertionError</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="c1">// Bad :(
</span></span></span><span class="line"><span class="cl"><span class="k">override</span> <span class="k">fun</span> <span class="nf">flush</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(!</span><span class="nc">Thread</span><span class="p">.</span><span class="n">holdsLock</span><span class="p">(</span><span class="k">this</span><span class="nd">@Http2Stream</span><span class="p">)</span> <span class="o">==</span> <span class="k">false</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">Http2Stream</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</span><span class="p">.</span><span class="n">desiredAssertionStatus</span><span class="p">())</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">throw</span> <span class="n">AssertionError</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="binary-incompatibility-challenges">Binary incompatibility challenges</h2>
<p><a href="https://twitter.com/JakeWharton">Jake Wharton</a> wrote in his usual in-depth detail about how the Kotlin <code>data</code> class modifier makes it a challenge to modify public API without breaking source and binary compatibility. Kotlin&rsquo;s sweet language features that provide things like default values in constructors and destructuring components become the very thing that inhibits binary compatibility.</p>
<p>Take about 10 minutes out and give Jake&rsquo;s article a read: <a href="https://jakewharton.com/public-api-challenges-in-kotlin/">Public API challenges in Kotlin</a>.</p>
<h2 id="summary">Summary</h2>
<p>While migrating from Java to Kotlin is great, there are many subtle differences between the languages that can blindside you and must be taken into account. It&rsquo;s more than likely that these problems may never affect you, but it&rsquo;s probably helpful to know what&rsquo;s up when they do :)</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Deploying Hugo sites with GitHub Actions</title>
      <link>https://msfjarvis.dev/posts/deploying-hugo-sites-with-github-actions/</link>
      <pubDate>Wed, 04 Dec 2019 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/deploying-hugo-sites-with-github-actions/</guid>
      <description>GitHub Actions are awesome! Learn how to use it for continuous delivery of your static sites.</description>
      <content:encoded><![CDATA[<p>For the longest time, I have used the <a href="http://github.com/abiosoft/caddy-git">caddy-git</a> middleware for <a href="https://caddyserver.com">caddyserver</a> to constantly deploy my <a href="https://gohugo.io">Hugo</a> site from <a href="https://github.com/msfjarvis/msfjarvis.dev">GitHub</a>.</p>
<p>But this approach had a few problems, notably force pushing (I know, shush) caused the repository to break because the plugin didn&rsquo;t support those. While not frequent, it was annoying enough to seek alternatives.</p>
<p>Enter <a href="https://github.com/features/actions">GitHub Actions</a>.</p>
<p>GitHub&rsquo;s in-built CI/CD solution is quite powerful and easily extensible. I decided to give it a shot and use it for automated deployments.</p>
<p>Now, my use case isn&rsquo;t the most straightforward. I maintain two sites out of the same source repository, one production site with all my published posts, and another with all my drafts enabled so I can check my WIP posts live to find any formatting mistakes I may have overlooked when writing through <a href="https://forestry.io">Forestry</a> or a text editor. I am also in the habit of creating and fixing my own problems so I prefer self-hosted solutions as and when possible.</p>
<h2 id="step-1---deployment">Step 1 - Deployment</h2>
<p>The first part of this endeavour involved finding a new way to move static assets to the server. I thought about emulating how <a href="http://github.com/abiosoft/caddy-git">caddy-git</a> works and using <code>ssh</code> to do a pull-and-build on my server itself. Then I found <a href="https://github.com/peaceiris/actions-hugo">this</a> action that allows me to install <code>hugo</code> in the container for the build. That&rsquo;s when I decided to do the building in the Actions pipeline and push built assets using <code>rsync</code>.</p>
<h2 id="step-2---execution">Step 2 - Execution</h2>
<p>To handle my two-sites-from-one-repo usecase, I setup a build staging -&gt; publish staging -&gt; build prod -&gt; publish prod pipeline.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Build staging</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">hugo --minify -DEFb=https://staging.msfjarvis.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy to staging</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">source $GITHUB_WORKSPACE/ci/deploy.sh</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ACTIONS_DEPLOY_KEY</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.ACTIONS_DEPLOY_KEY }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">SSH_USERNAME</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.SSH_USERNAME }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">SERVER_ADDRESS</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.SERVER_ADDRESS }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">SERVER_DESTINATION</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.SERVER_DESTINATION_STAGING }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">SSH_PORT</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.SSH_PORT }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Build prod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">hugo --minify</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy to prod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">source $GITHUB_WORKSPACE/ci/deploy.sh</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ACTIONS_DEPLOY_KEY</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.ACTIONS_DEPLOY_KEY }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">SSH_USERNAME</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.SSH_USERNAME }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">SERVER_ADDRESS</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.SERVER_ADDRESS }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">SERVER_DESTINATION</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.SERVER_DESTINATION_PROD }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">SSH_PORT</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.SSH_PORT }}</span><span class="w">
</span></span></span></code></pre></div><p>You can find the <code>ci/deploy.sh</code> script <a href="https://github.com/msfjarvis/msfjarvis.dev/blob/src/ci/deploy.sh">here</a>. It&rsquo;s a very basic script that sets up the SSH authentication and rsync&rsquo;s the built site over.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Publishing an Android library to GitHub Packages</title>
      <link>https://msfjarvis.dev/posts/publishing-an-android-library-to-github-packages/</link>
      <pubDate>Thu, 21 Nov 2019 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/publishing-an-android-library-to-github-packages/</guid>
      <description>GitHub recently rolled out Packages to the general public, allowing the entire develop-test-deploy pipeline to get centralized at GitHub. Learn how to use it to publish your Android library packages.</description>
      <content:encoded><![CDATA[<blockquote>
<p>UPDATE(06/06/2020): The Android Gradle Plugin supports Gradle&rsquo;s inbuilt <code>maven-publish</code> plugin since version 4.0.0, so I&rsquo;ve added the updated process for utilising it at the beginning of this guide. The previous post follows that section.</p>
</blockquote>
<p>GitHub released the Package Registry beta in May of this year, and graduated it to public availability in Universe 2019, rebranded as <a href="https://github.com/features/packages" title="GitHub Packages">GitHub Packages</a>. It supports NodeJS, Docker, Maven, Gradle, NuGet, and RubyGems. That&rsquo;s a LOT of ground covered for a service that&rsquo;s about one year old.</p>
<p>Naturally, I was excited to try this out. The <a href="https://help.github.com/en/github/managing-packages-with-github-packages/about-github-packages">documentation</a> is by no means lacking, but the <a href="https://help.github.com/en/github/managing-packages-with-github-packages/configuring-gradle-for-use-with-github-packages">official instructions</a> for using Packages with Gradle do not work for Android libraries. To make it compatible with Android libraries, some small but non-obvious edits are needed which I&rsquo;ve documented here for everybody&rsquo;s benefit.</p>
<blockquote>
<p>GitHub Packages currently does <strong>NOT</strong> support unauthenticated access to packages, which means you will always require a personal access token with the <code>read:packages</code> scope to be able to download packages during build. I emailed GitHub support about this, and their reply is attached at the end of this post.</p>
</blockquote>
<p>I&rsquo;ve also created a <a href="https://github.com/msfjarvis/github-packages-deployment-sample/">sample repository</a> with incremental commits corresponding to the steps given below, for people who prefer to see the code directly.</p>
<p>To be able to deploy packages, you will require a Personal Access Token from GitHub with the <code>write:packages</code> scope. Follow the steps <a href="https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token#creating-a-token">here</a> to create the token if you have never done so before.</p>
<h3 id="for-agp--400">For AGP &gt;= 4.0.0</h3>
<p>All you need to do is ensure you&rsquo;re on at least Gradle 6.5 and AGP 4.0.0, then configure as follows.</p>
<p>For Groovy:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-groovy" data-lang="groovy"><span class="line"><span class="cl"><span class="n">apply</span> <span class="nl">plugin:</span> <span class="s1">&#39;maven-publish&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">afterEvaluate</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">publishing</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">repositories</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">maven</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">name</span> <span class="o">=</span> <span class="s2">&#34;GitHubPackages&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">url</span> <span class="o">=</span> <span class="n">uri</span><span class="o">(</span><span class="s2">&#34;https://maven.pkg.github.com/msfjarvis/github-packages-deployment-sample&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">credentials</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">          <span class="n">username</span> <span class="o">=</span> <span class="n">project</span><span class="o">.</span><span class="na">findProperty</span><span class="o">(</span><span class="s2">&#34;gpr.user&#34;</span><span class="o">)</span> <span class="o">?:</span> <span class="n">System</span><span class="o">.</span><span class="na">getenv</span><span class="o">(</span><span class="s2">&#34;USERNAME&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">          <span class="n">password</span> <span class="o">=</span> <span class="n">project</span><span class="o">.</span><span class="na">findProperty</span><span class="o">(</span><span class="s2">&#34;gpr.key&#34;</span><span class="o">)</span> <span class="o">?:</span> <span class="n">System</span><span class="o">.</span><span class="na">getenv</span><span class="o">(</span><span class="s2">&#34;PASSWORD&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">        <span class="o">}</span>
</span></span><span class="line"><span class="cl">      <span class="o">}</span>
</span></span><span class="line"><span class="cl">    <span class="o">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">publications</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">release</span><span class="o">(</span><span class="n">MavenPublication</span><span class="o">)</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">from</span> <span class="n">components</span><span class="o">.</span><span class="na">release</span>
</span></span><span class="line"><span class="cl">        <span class="n">groupId</span> <span class="o">=</span> <span class="s2">&#34;$GROUP&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">artifactId</span> <span class="o">=</span> <span class="s2">&#34;deployment-sample-library&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">version</span> <span class="o">=</span> <span class="s2">&#34;$VERSION&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="o">}</span>
</span></span><span class="line"><span class="cl">    <span class="o">}</span>
</span></span><span class="line"><span class="cl">  <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><p>For Kotlin:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="n">plugins</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">id</span><span class="p">(</span><span class="s2">&#34;maven-publish&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">afterEvaluate</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">publishing</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">repositories</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">maven</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">name</span> <span class="p">=</span> <span class="s2">&#34;GitHubPackages&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">url</span> <span class="p">=</span> <span class="n">uri</span><span class="p">(</span><span class="s2">&#34;https://maven.pkg.github.com/msfjarvis/github-packages-deployment-sample&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">credentials</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="n">username</span> <span class="p">=</span> <span class="n">project</span><span class="p">.</span><span class="n">findProperty</span><span class="p">(</span><span class="s2">&#34;gpr.user&#34;</span><span class="p">)</span> <span class="o">?:</span> <span class="nc">System</span><span class="p">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&#34;USERNAME&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="n">password</span> <span class="p">=</span> <span class="n">project</span><span class="p">.</span><span class="n">findProperty</span><span class="p">(</span><span class="s2">&#34;gpr.key&#34;</span><span class="p">)</span> <span class="o">?:</span> <span class="nc">System</span><span class="p">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&#34;PASSWORD&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">publications</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="c1">// Simple convenience function to hide the nullability of `findProperty`.
</span></span></span><span class="line"><span class="cl">      <span class="k">private</span> <span class="k">fun</span> <span class="nf">getProperty</span><span class="p">(</span><span class="n">key</span><span class="p">:</span> <span class="n">String</span><span class="p">):</span> <span class="n">String</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">findProperty</span><span class="p">(</span><span class="n">key</span><span class="p">)</span><span class="o">?.</span><span class="n">toString</span><span class="p">()</span> <span class="o">?:</span> <span class="n">error</span><span class="p">(</span><span class="s2">&#34;Failed to find property for </span><span class="si">$key</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="n">create</span><span class="p">&lt;</span><span class="n">MavenPublication</span><span class="p">&gt;(</span><span class="s2">&#34;release&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">from</span><span class="p">(</span><span class="n">components</span><span class="p">.</span><span class="n">getByName</span><span class="p">(</span><span class="s2">&#34;release&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="n">groupId</span> <span class="p">=</span> <span class="n">getProperty</span><span class="p">(</span><span class="s2">&#34;GROUP&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">artifactId</span> <span class="p">=</span> <span class="s2">&#34;deployment-sample-library&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">version</span> <span class="p">=</span> <span class="n">getProperty</span><span class="p">(</span><span class="s2">&#34;VERSION&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Then, set the <code>GROUP</code> and <code>VERSION</code> properties in <code>gradle.properties</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-groovy" data-lang="groovy"><span class="line"><span class="cl"><span class="n">GROUP</span><span class="o">=</span><span class="n">msfjarvis</span>
</span></span><span class="line"><span class="cl"><span class="n">VERSION</span><span class="o">=</span><span class="mf">0.1</span><span class="o">.</span><span class="mi">0</span><span class="o">-</span><span class="n">SNAPSHOT</span>
</span></span></code></pre></div><p>And that should be it! You can check the migration commit <a href="https://github.com/msfjarvis/github-packages-deployment-sample/commit/260fd3154fd393d3969afd048dc2c77d03619b1d">here</a>.</p>
<p>When you are ready to publish, run <code>./gradlew -Pgpr.user=&lt;username&gt; -Pgpr.key=&lt;personal access token&gt; publish</code> from your repository and everything should correctly deploy.</p>
<h3 id="for-agp--400-1">For AGP &lt; 4.0.0</h3>
<h4 id="step-1">Step 1</h4>
<p>Copy the official integration step from GitHub&rsquo;s <a href="https://help.github.com/en/github/managing-packages-with-github-packages/configuring-gradle-for-use-with-github-packages#authenticating-with-a-personal-access-token">guide</a>, into your Android library&rsquo;s <code>build.gradle</code> / <code>build.gradle.kts</code>. If you try to run <code>./gradlew publish</code> now, you&rsquo;ll run into errors. We&rsquo;ll be fixing that shortly. [<a href="https://github.com/msfjarvis/github-packages-deployment-sample/commit/d69235577a1d4345cecb364a3a3d366bf894c5a6">Commit link</a>]</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">--- library/build.gradle
</span></span></span><span class="line"><span class="cl"><span class="gi">+++ library/build.gradle
</span></span></span><span class="line"><span class="cl"><span class="gu">@@ -1,5 +1,6 @@
</span></span></span><span class="line"><span class="cl"> apply plugin: &#34;com.android.library&#34;
</span></span><span class="line"><span class="cl"> apply plugin: &#34;kotlin-android&#34;
</span></span><span class="line"><span class="cl"><span class="gi">+apply plugin: &#34;maven-publish&#34;
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> apply from: &#34;../dependencies.gradle&#34;
</span></span><span class="line"><span class="cl"> // apply from: &#34;../bintrayconfig.gradle&#34;
</span></span><span class="line"><span class="cl"><span class="gu">@@ -28,6 +29,24 @@ android {
</span></span></span><span class="line"><span class="cl">   }
</span></span><span class="line"><span class="cl"> }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gi">+publishing {
</span></span></span><span class="line"><span class="cl"><span class="gi">+  repositories {
</span></span></span><span class="line"><span class="cl"><span class="gi">+    maven {
</span></span></span><span class="line"><span class="cl"><span class="gi">+      name = &#34;GitHubPackages&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+      url = uri(&#34;https://maven.pkg.github.com/msfjarvis/github-packages-deployment-sample&#34;)
</span></span></span><span class="line"><span class="cl"><span class="gi">+      credentials {
</span></span></span><span class="line"><span class="cl"><span class="gi">+        username = project.findProperty(&#34;gpr.user&#34;) ?: System.getenv(&#34;USERNAME&#34;)
</span></span></span><span class="line"><span class="cl"><span class="gi">+        password = project.findProperty(&#34;gpr.key&#34;) ?: System.getenv(&#34;PASSWORD&#34;)
</span></span></span><span class="line"><span class="cl"><span class="gi">+      }
</span></span></span><span class="line"><span class="cl"><span class="gi">+    }
</span></span></span><span class="line"><span class="cl"><span class="gi">+  }
</span></span></span><span class="line"><span class="cl"><span class="gi">+  publications {
</span></span></span><span class="line"><span class="cl"><span class="gi">+    gpr(MavenPublication) {
</span></span></span><span class="line"><span class="cl"><span class="gi">+      from(components.java)
</span></span></span><span class="line"><span class="cl"><span class="gi">+    }
</span></span></span><span class="line"><span class="cl"><span class="gi">+  }
</span></span></span><span class="line"><span class="cl"><span class="gi">+}
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"> dependencies {
</span></span><span class="line"><span class="cl">   api deps.support.app_compat
</span></span><span class="line"><span class="cl">   implementation deps.kotlin.stdlib8
</span></span></code></pre></div><h4 id="step-2">Step 2</h4>
<p>Switch out the <code>maven-publish</code> plugin with <a href="https://github.com/wupdigital/android-maven-publish">this</a> one. It provides us an Android component that&rsquo;s compatible with publications and precisely what we need. [<a href="https://github.com/msfjarvis/github-packages-deployment-sample/commit/1452c4a0c15d394b73dc3384f02834788dfe1bda">Commit link</a>]</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">--- build.gradle
</span></span></span><span class="line"><span class="cl"><span class="gi">+++ build.gradle
</span></span></span><span class="line"><span class="cl"><span class="gu">@@ -14,6 +14,7 @@ buildscript {
</span></span></span><span class="line"><span class="cl">     classpath deps.gradle_plugins.kotlin
</span></span><span class="line"><span class="cl">     classpath deps.gradle_plugins.spotless
</span></span><span class="line"><span class="cl">     classpath deps.gradle_plugins.versions
</span></span><span class="line"><span class="cl"><span class="gi">+    classpath deps.gradle_plugins.android_maven_publish
</span></span></span><span class="line"><span class="cl">   }
</span></span><span class="line"><span class="cl"> }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gd">--- dependencies.gradle
</span></span></span><span class="line"><span class="cl"><span class="gi">+++ dependencies.gradle
</span></span></span><span class="line"><span class="cl"><span class="gu">@@ -12,7 +12,8 @@ ext.deps = [
</span></span></span><span class="line"><span class="cl">         spotless: &#34;com.diffplug.spotless:spotless-plugin-gradle:3.26.0&#34;,
</span></span><span class="line"><span class="cl">         versions: &#34;com.github.ben-manes:gradle-versions-plugin:0.27.0&#34;,
</span></span><span class="line"><span class="cl">         bintray_release: &#34;com.novoda:bintray-release:0.9.1&#34;,
</span></span><span class="line"><span class="cl"><span class="gd">-        kotlin: &#34;org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.60&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+        kotlin: &#34;org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.60&#34;,
</span></span></span><span class="line"><span class="cl"><span class="gi">+        android_maven_publish: &#34;digital.wup:android-maven-publish:3.6.2&#34;
</span></span></span><span class="line"><span class="cl">     ],
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">     kotlin: [
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gd">--- library/build.gradle
</span></span></span><span class="line"><span class="cl"><span class="gi">+++ library/build.gradle
</span></span></span><span class="line"><span class="cl"><span class="gu">@@ -1,6 +1,6 @@
</span></span></span><span class="line"><span class="cl"> apply plugin: &#34;com.android.library&#34;
</span></span><span class="line"><span class="cl"> apply plugin: &#34;kotlin-android&#34;
</span></span><span class="line"><span class="cl"><span class="gd">-apply plugin: &#34;maven-publish&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+apply plugin: &#34;digital.wup.android-maven-publish&#34;
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> apply from: &#34;../dependencies.gradle&#34;
</span></span><span class="line"><span class="cl"> // apply from: &#34;../bintrayconfig.gradle&#34;
</span></span></code></pre></div><h4 id="step-3">Step 3</h4>
<p>Switch to using the <code>android</code> component provided by <code>wup.digital.android-maven-publish</code>. This is the one we require to be able to upload an <a href="https://developer.android.com/studio/projects/android-library">AAR</a> artifact. [<a href="https://github.com/msfjarvis/github-packages-deployment-sample/commit/7cc6fcd6ffa5774433bce76ac6929435dbbb77cc">Commit link</a>]</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">--- library/build.gradle
</span></span></span><span class="line"><span class="cl"><span class="gi">+++ library/build.gradle
</span></span></span><span class="line"><span class="cl"><span class="gu">@@ -42,7 +42,7 @@ publishing {
</span></span></span><span class="line"><span class="cl">   }
</span></span><span class="line"><span class="cl">   publications {
</span></span><span class="line"><span class="cl">     gpr(MavenPublication) {
</span></span><span class="line"><span class="cl"><span class="gd">-      from(components.java)
</span></span></span><span class="line"><span class="cl"><span class="gi">+      from(components.android)
</span></span></span><span class="line"><span class="cl">     }
</span></span><span class="line"><span class="cl">   }
</span></span><span class="line"><span class="cl"> }
</span></span></code></pre></div><h4 id="step-4">Step 4</h4>
<p>Every Gradle/Maven dependency&rsquo;s address has three attributes, a group ID, an artifact ID, and a version.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-groovy" data-lang="groovy"><span class="line"><span class="cl"><span class="n">implementation</span> <span class="s1">&#39;com.example:my-fancy-library:1.0.0&#39;</span>
</span></span></code></pre></div><p>Here:</p>
<ul>
<li>Group ID: <code>com.example</code></li>
<li>Artifact ID: <code>my-fancy-library</code></li>
<li>Version: <code>1.0.0</code></li>
</ul>
<p>We&rsquo;ll need to configure these too. I prefer using the <code>gradle.properties</code> file for this purpose since it&rsquo;s very easy to access variables from it, but if you have a favorite way of configuring build properties, use that instead! [<a href="https://github.com/msfjarvis/github-packages-deployment-sample/commit/cee74a5e0b3b76d1d7a2d4eb9636d80fb1db49d6">Commit link</a>]</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">--- gradle.properties
</span></span></span><span class="line"><span class="cl"><span class="gi">+++ gradle.properties
</span></span></span><span class="line"><span class="cl"><span class="gu">@@ -19,3 +19,7 @@ android.useAndroidX=true
</span></span></span><span class="line"><span class="cl"> android.enableJetifier=true
</span></span><span class="line"><span class="cl"> # Kotlin code style for this project: &#34;official&#34; or &#34;obsolete&#34;:
</span></span><span class="line"><span class="cl"> kotlin.code.style=official
</span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+# Publishing config
</span></span></span><span class="line"><span class="cl"><span class="gi">+GROUP=msfjarvis
</span></span></span><span class="line"><span class="cl"><span class="gi">+VERSION=0.1.0-SNAPSHOT
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gd">--- library/build.gradle
</span></span></span><span class="line"><span class="cl"><span class="gi">+++ library/build.gradle
</span></span></span><span class="line"><span class="cl"><span class="gu">@@ -43,6 +43,10 @@ publishing {
</span></span></span><span class="line"><span class="cl">   publications {
</span></span><span class="line"><span class="cl">     gpr(MavenPublication) {
</span></span><span class="line"><span class="cl">       from(components.android)
</span></span><span class="line"><span class="cl"><span class="gi">+      groupId &#34;$GROUP&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+      artifactId &#34;deployment-sample-library&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+      // Use your configured version outside CI, the SHA of the top commit inside.
</span></span></span><span class="line"><span class="cl"><span class="gi">+      version System.env[&#39;GITHUB_SHA&#39;] == null ? &#34;$VERSION&#34; : System.env[&#39;GITHUB_SHA&#39;]
</span></span></span><span class="line"><span class="cl">     }
</span></span><span class="line"><span class="cl">   }
</span></span><span class="line"><span class="cl"> }
</span></span></code></pre></div><h4 id="step-5">Step 5</h4>
<p>Now all that&rsquo;s left to do is configure GitHub Actions. Go to the Secrets menu in your repository&rsquo;s settings, then create a <code>PACKAGES_TOKEN</code> secret and provide the access token you generated earlier. Head over to the <a href="https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets#creating-encrypted-secrets">documentation</a> for Secrets if you wanna know how this works under the hood.</p>
<p>Now, let&rsquo;s add the actual configuration that&rsquo;ll get Actions up and running.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">--- /dev/null
</span></span></span><span class="line"><span class="cl"><span class="gi">+++ .github/workflows/publish_snapshot.yml
</span></span></span><span class="line"><span class="cl"><span class="gu">@@ -0,0 +1,13 @@
</span></span></span><span class="line"><span class="cl"><span class="gi">+name: &#34;Release per-commit snapshots&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+on: push
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+jobs:
</span></span></span><span class="line"><span class="cl"><span class="gi">+  setup-android:
</span></span></span><span class="line"><span class="cl"><span class="gi">+    runs-on: ubuntu-latest
</span></span></span><span class="line"><span class="cl"><span class="gi">+    steps:
</span></span></span><span class="line"><span class="cl"><span class="gi">+    - uses: actions/checkout@master
</span></span></span><span class="line"><span class="cl"><span class="gi">+    - name: Publish snapshot
</span></span></span><span class="line"><span class="cl"><span class="gi">+      run: ./gradlew publish
</span></span></span><span class="line"><span class="cl"><span class="gi">+      env:
</span></span></span><span class="line"><span class="cl"><span class="gi">+        USERNAME: msfjarvis
</span></span></span><span class="line"><span class="cl"><span class="gi">+        PASSWORD: ${{ secrets.PACKAGES_TOKEN }}
</span></span></span></code></pre></div><p>That&rsquo;s it! Once you push to GitHub, you&rsquo;ll see the <a href="https://github.com/msfjarvis/github-packages-deployment-sample/commit/42e1f6609bf9f2abe8e181296a57d86df648b4d4/checks?check_suite_id=322323808">action running</a> in your repository&rsquo;s Actions tab and a <a href="https://github.com/msfjarvis/github-packages-deployment-sample/packages/60429">corresponding package</a> in the Packages tab once the workflow finishes executing.</p>
<h3 id="closing-notes">Closing notes</h3>
<p>The requirement to authenticate for packages is a significant problem with GitHub Packages&rsquo; adoption, giving an edge to solutions like <a href="https://jitpack.io">JitPack</a> which handle the entire process automagically. As mentioned earlier, I did contact GitHub support about it and got this back.</p>
<p><img alt="GitHub support reply about authentication requirement for packages" loading="lazy" src="/posts/publishing-an-android-library-to-github-packages/github_packages_support_response.webp"></p>
<p>My interpretation of this is quite simply that <strong>it&rsquo;s gonna take a while</strong>. I hope not :)</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Why I went back to the Gradle Groovy DSL</title>
      <link>https://msfjarvis.dev/posts/why-i-went-back-to-the-gradle-groovy-dsl/</link>
      <pubDate>Fri, 25 Oct 2019 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/why-i-went-back-to-the-gradle-groovy-dsl/</guid>
      <description>I was an early adopter of the Gradle Kotlin DSL, deploying it to multiple Android projects of mine, but lately it has been more trouble than I could care for. Here are my grievances with it.</description>
      <content:encoded><![CDATA[<p>About an year ago when I first discovered the <a href="https://docs.gradle.org/current/userguide/kotlin_dsl.html">Gradle Kotlin DSL</a>, I was very quick to <a href="https://github.com/msfjarvis/viscerion/commit/c16d11a816c3c7e3f7bab51ef2f32569b6b657bf">jump</a> <a href="https://github.com/android-password-store/Android-Password-Store/commit/3c06063153d0b7f71998128dc6fb4e5967e33624">on</a> <a href="https://github.com/substratum/substratum/commit/ebff9a3a88781d093565526b171d9d5b8e9c1bed">that</a> <a href="https://github.com/substratum/substratum/commit/5065e082055cde19e41ee02920ca07d0e33c89f5">train</a>. Now it feels like a mistake.</p>
<p>The initial premise of the Gradle Kotlin DSL was very cool. You get first class code completion in the IDE, and you get to write Kotlin rather than the arguably weird Groovy. People were excited to finally be able to write complex build logic using the <code>buildSrc</code> functionality that this change introduced.</p>
<p>However the dream slowly started fading as more and more people started using the Kotlin DSL and the shortcomings became more apparent. My grievances with the Kotlin DSL are multifold as I&rsquo;ll detail below.</p>
<p>Just a disclaimer, This post is not meant to completely trash the Kotlin DSL&rsquo;s usability. It has it&rsquo;s own very great benefits and people who leverage those should continue using it and disregard this post :-)</p>
<h3 id="build-times">Build times</h3>
<p>The Gradle Kotlin DSL inflates build times <em>significantly</em>. Compiling <code>buildSrc</code> and all the <code>*.gradle.kts</code> files for my <a href="http://github.com/msfjarvis/viscerion/tree/1ea6f07f8219aa42139977f37ebbcb230d7f78e7" title="app">app</a> takes upto 10 seconds longer than the Groovy DSL. Couple that with the fact that changing any file from <code>buildSrc</code> invalidated the entire compiler cache for me made iterative development extremely painful.</p>
<h3 id="half-baked-api-surface">Half-baked API surface</h3>
<p>Gradle doesn&rsquo;t seem to have invested any actual time in converting the original Groovy APIs into Kotlin-friendly versions before they peddled the Kotlin DSL to us. Check the samples below and decide for yourself.</p>
<p>Groovy</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-groovy" data-lang="groovy"><span class="line"><span class="cl"><span class="n">android</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">compileSdkVersion</span> <span class="mi">29</span>
</span></span><span class="line"><span class="cl">  <span class="n">buildToolsVersion</span> <span class="o">=</span> <span class="s1">&#39;29.0.2&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="n">defaultConfig</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">minSdkVersion</span> <span class="mi">21</span>
</span></span><span class="line"><span class="cl">    <span class="n">targetSdkVersion</span> <span class="mi">29</span>
</span></span><span class="line"><span class="cl">  <span class="o">}</span>
</span></span><span class="line"><span class="cl">  <span class="n">buildTypes</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">minifyEnabled</span> <span class="o">=</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">  <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">dependencies</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">implementation</span><span class="o">(</span><span class="s1">&#39;my.company:fancy.library:1.1.1&#39;</span><span class="o">)</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">force</span> <span class="o">=</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">  <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><p>Kotlin</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-groovy" data-lang="groovy"><span class="line"><span class="cl"><span class="n">android</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">compileSdkVersion</span><span class="o">(</span><span class="mi">29</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">buildToolsVersion</span> <span class="o">=</span> <span class="s2">&#34;29.0.2&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">defaultConfig</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">minSdkVersion</span><span class="o">(</span><span class="mi">21</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">targetSdkVersion</span><span class="o">(</span><span class="mi">29</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">}</span>
</span></span><span class="line"><span class="cl">  <span class="n">buildTypes</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">isMinifyEnabled</span> <span class="o">=</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">  <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">dependencies</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">implementation</span><span class="o">(</span><span class="s1">&#39;my.company:fancy.library:1.1.1&#39;</span><span class="o">)</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">isForce</span> <span class="o">=</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">  <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><p>I am definitely biased here, but this is not how an idiomatic Kotlin API looks like.</p>
<p>What we should have gotten</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-groovy" data-lang="groovy"><span class="line"><span class="cl"><span class="n">android</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">compileSdkVersion</span> <span class="o">=</span> <span class="mi">29</span>
</span></span><span class="line"><span class="cl">  <span class="n">buildToolsVersion</span> <span class="o">=</span> <span class="s2">&#34;29.0.2&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">defaultConfig</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">minSdkVersion</span> <span class="o">=</span> <span class="mi">21</span>
</span></span><span class="line"><span class="cl">    <span class="n">targetSdkVersion</span> <span class="o">=</span> <span class="mi">29</span>
</span></span><span class="line"><span class="cl">  <span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">buildTypes</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">minifyEnabled</span> <span class="o">=</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">   <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">dependencies</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">implementation</span><span class="o">(</span><span class="s1">&#39;my.company:fancy.library:1.1.1&#39;</span><span class="o">)</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">force</span> <span class="o">=</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">  <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><p>Property access syntax and discoverable variable names should have been the norm since day one for it to actually be a good Kotlin DSL.</p>
<h3 id="complexity">Complexity</h3>
<p>The Kotlin DSL is not very well documented outside Gradle&rsquo;s bits and pieces in documentation. Things like <a href="https://github.com/msfjarvis/viscerion/commit/c851571e33189c345329ea3934ad1af15edbe6fb" title="this">this</a> were incredibly problematic to implement in the Kotlin DSL, at least for me and I found it to be incredibly frustrating.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Again, these are my pain points with the Kotlin DSL. I still use it for some of my projects but I am not going to use it in new projects until Gradle addresses these pains.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>#TeachingKotlin Part 2 - Variables</title>
      <link>https://msfjarvis.dev/posts/teaching-kotlin--variables/</link>
      <pubDate>Mon, 30 Sep 2019 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/teaching-kotlin--variables/</guid>
      <description>The second post in #TeachingKotlin series, this post goes over Kotlin&amp;rsquo;s variables and their attributes, like visibility and getters/setters.</description>
      <content:encoded><![CDATA[<p>Even the variables in Kotlin are supercharged!</p>
<p>Let&rsquo;s start with a simple <a href="https://kotlinlang.org/docs/reference/data-classes.html#data-classes">data class</a> and see how the variables in there behave.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="k">data</span> <span class="k">class</span> <span class="nc">Student</span><span class="p">(</span><span class="k">val</span> <span class="py">name</span><span class="p">:</span> <span class="n">String</span><span class="p">,</span> <span class="k">val</span> <span class="py">age</span><span class="p">:</span> <span class="n">Int</span><span class="p">,</span> <span class="k">val</span> <span class="py">subjects</span><span class="p">:</span> <span class="n">ArrayList</span><span class="p">&lt;</span><span class="n">String</span><span class="p">&gt;)</span>
</span></span></code></pre></div><p>To use the variables in this class, Kotlin let&rsquo;s you directly use the dot notation for accessing.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="p">&gt;&gt;&gt;</span> <span class="k">val</span> <span class="py">s1</span> <span class="p">=</span> <span class="n">Student</span><span class="p">(</span><span class="s2">&#34;Keith Hernandez&#34;</span><span class="p">,</span> <span class="mi">21</span><span class="p">,</span> <span class="n">arrayListOf</span><span class="p">(</span><span class="s2">&#34;Mathematics&#34;</span><span class="p">,</span> <span class="s2">&#34;Social Studies&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="p">&gt;&gt;&gt;</span> <span class="n">println</span><span class="p">(</span><span class="n">s1</span><span class="p">.</span><span class="n">name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">Keith</span> <span class="n">Hernandez</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">&gt;&gt;&gt;</span> <span class="n">println</span><span class="p">(</span><span class="n">s1</span><span class="p">)</span> <span class="c1">// data classes automatically generate `toString` and `hashCode`
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">Student</span><span class="p">(</span><span class="n">name</span><span class="p">=</span><span class="n">Keith</span> <span class="n">Hernandez</span><span class="p">,</span> <span class="n">age</span><span class="p">=</span><span class="mi">21</span><span class="p">,</span> <span class="n">subjects</span><span class="p">=[</span><span class="n">Mathematics</span><span class="p">,</span> <span class="n">Social</span> <span class="n">Studies</span><span class="p">])</span>
</span></span></code></pre></div><p>For Java callers, Kotlin also generates getters and setter methods.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">final</span><span class="w"> </span><span class="n">Student</span><span class="w"> </span><span class="n">s1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">Student</span><span class="p">(</span><span class="s">&#34;Keith Hernandez&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">21</span><span class="p">,</span><span class="w"> </span><span class="n">arrayListOf</span><span class="p">(</span><span class="s">&#34;Mathematics&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;Social Studies&#34;</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="n">s1</span><span class="p">.</span><span class="na">getName</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">System</span><span class="p">.</span><span class="na">out</span><span class="p">.</span><span class="na">println</span><span class="p">(</span><span class="n">s1</span><span class="p">);</span><span class="w">
</span></span></span></code></pre></div><p>The same properties apply to variables in non-data classes as well.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="p">&gt;&gt;&gt;</span> <span class="k">class</span> <span class="nc">Item</span><span class="p">(</span><span class="n">id</span><span class="p">:</span> <span class="n">Int</span><span class="p">,</span> <span class="n">name</span><span class="p">:</span> <span class="n">String</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">..</span><span class="p">.</span>     <span class="k">val</span> <span class="py">itemId</span> <span class="p">=</span> <span class="n">id</span>
</span></span><span class="line"><span class="cl"><span class="o">..</span><span class="p">.</span>     <span class="k">val</span> <span class="py">itemName</span> <span class="p">=</span> <span class="n">name</span>
</span></span><span class="line"><span class="cl"><span class="o">..</span><span class="p">.</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">&gt;&gt;&gt;</span> <span class="k">val</span> <span class="py">item</span> <span class="p">=</span> <span class="n">Item</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="s2">&#34;Bricks&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">&gt;&gt;&gt;</span> <span class="n">println</span><span class="p">(</span><span class="n">item</span><span class="p">.</span><span class="n">itemId</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="mi">0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">&gt;&gt;&gt;</span> <span class="n">println</span><span class="p">(</span><span class="n">item</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">Line_4</span><span class="err">$</span><span class="n">Item</span><span class="err">@</span><span class="mf">46f</span><span class="n">b460a</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">&gt;&gt;</span> <span class="p">&gt;</span>
</span></span></code></pre></div><p>As you can notice, the <code>toString</code> implementation is not identical to our data classes but that&rsquo;s a topic for another post. Back to variables!</p>
<h2 id="customizing-getters-and-setters">Customizing getters and setters</h2>
<p>While Kotlin creates getters and setters automatically, we can customize their behavior.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Item</span><span class="p">(</span><span class="n">id</span><span class="p">:</span> <span class="n">Int</span><span class="p">,</span> <span class="n">name</span><span class="p">:</span> <span class="n">String</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">var</span> <span class="py">itemId</span> <span class="p">=</span> <span class="n">id</span>
</span></span><span class="line"><span class="cl">    <span class="k">var</span> <span class="py">itemName</span> <span class="p">=</span> <span class="n">name</span>
</span></span><span class="line"><span class="cl">    <span class="k">var</span> <span class="py">currentState</span><span class="p">:</span> <span class="n">Pair</span><span class="p">&lt;</span><span class="n">Int</span><span class="p">,</span> <span class="n">String</span><span class="p">&gt;</span> <span class="p">=</span> <span class="n">Pair</span><span class="p">(</span><span class="n">itemId</span><span class="p">,</span> <span class="n">itemName</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">set</span><span class="p">(</span><span class="k">value</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="n">itemId</span> <span class="p">=</span> <span class="k">value</span><span class="p">.</span><span class="n">first</span>
</span></span><span class="line"><span class="cl">            <span class="n">itemName</span> <span class="p">=</span> <span class="k">value</span><span class="p">.</span><span class="n">second</span>
</span></span><span class="line"><span class="cl">            <span class="k">field</span> <span class="p">=</span> <span class="k">value</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">override</span> <span class="k">fun</span> <span class="nf">toString</span><span class="p">()</span> <span class="p">:</span> <span class="n">String</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="s2">&#34;id=</span><span class="si">$itemId</span><span class="s2">,name=</span><span class="si">$itemName</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Let&rsquo;s take this for a spin in the Kotlin REPL and see how our <code>currentState</code> field behaves.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="p">&gt;&gt;&gt;</span> <span class="k">val</span> <span class="py">item</span> <span class="p">=</span> <span class="n">Item</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="s2">&#34;Nails&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">&gt;&gt;&gt;</span> <span class="n">println</span><span class="p">(</span><span class="n">item</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">id</span><span class="p">=</span><span class="mi">0</span><span class="p">,</span><span class="n">name</span><span class="p">=</span><span class="n">Nails</span>
</span></span><span class="line"><span class="cl"><span class="p">&gt;&gt;&gt;</span> <span class="n">item</span><span class="p">.</span><span class="n">currentState</span> <span class="p">=</span> <span class="n">Pair</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="s2">&#34;Bricks&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">&gt;&gt;&gt;</span> <span class="n">println</span><span class="p">(</span><span class="n">item</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">id</span><span class="p">=</span><span class="mi">1</span><span class="p">,</span><span class="n">name</span><span class="p">=</span><span class="n">Bricks</span>
</span></span></code></pre></div><p>Notice how setting a new value to currentState mutates the other variables as well? That&rsquo;s because of our custom setter. These setters are identical to a normal top-level function except a reference to the field in question is available as the variable <code>field</code> for manipulation.</p>
<h2 id="visibility-modifiers">Visibility modifiers</h2>
<p>Kotlin&rsquo;s visibility modifiers aren&rsquo;t very well explained. There&rsquo;s the standard <code>public</code>, <code>private</code> and <code>protected</code>, but also the new <code>inner</code> and <code>internal</code>. I&rsquo;ll attempt to fill in those gaps.</p>
<h3 id="inner"><code>inner</code></h3>
<p><code>inner</code> is a modifier that only applies to classes declared within another one. It allows you to access members of the enclosing class. A sample might help explain this better.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Outer</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">private</span> <span class="k">val</span> <span class="py">bar</span><span class="p">:</span> <span class="n">Int</span> <span class="p">=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">    <span class="k">inner</span> <span class="k">class</span> <span class="nc">Inner</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">fun</span> <span class="nf">foo</span><span class="p">()</span> <span class="p">=</span> <span class="n">bar</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">val</span> <span class="py">demo</span> <span class="p">=</span> <span class="n">Outer</span><span class="p">().</span><span class="n">Inner</span><span class="p">().</span><span class="n">foo</span><span class="p">()</span> <span class="c1">// == 1
</span></span></span></code></pre></div><p>The keyword <code>this</code> does not behave as some would normally expect in inner classes, go through the Kotlin documentation for <code>this</code> <a href="https://kotlinlang.org/docs/reference/this-expressions.html">here</a> and I&rsquo;ll be happy to answer any further questions :)</p>
<h3 id="internal"><code>internal</code></h3>
<p><code>internal</code> applies to methods and properties in classes. It makes the field/method &lsquo;module-local&rsquo;, allowing it to be accessed within the same module and nowhere else. A module in this context is a logical compilation unit, like a Gradle subproject.</p>
<p>That&rsquo;s all for today! Hope you&rsquo;re liking the series so far. I&rsquo;d love to hear feedback on what you want me to cover next and how to improve what I write :)</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>#TeachingKotlin Part 1 - Classes and Objects and everything in between</title>
      <link>https://msfjarvis.dev/posts/teaching-kotlin--classes-and-objects/</link>
      <pubDate>Mon, 23 Sep 2019 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/teaching-kotlin--classes-and-objects/</guid>
      <description>Part 1 of my #TeachingKotlin, this post goes over Kotlin classes, objects and how things like finality and staticity vary between Java and Kotlin.</description>
      <content:encoded><![CDATA[<p>Classes in Kotlin closely mimic their Java counterparts in implementation, with some crucial changes that I will attempt to outline here.</p>
<p>Let&rsquo;s declare two identical classes in Kotlin and Java as a starting point. We&rsquo;ll be making changes to them alongside to show how different patterns are implemented in the two languages.</p>
<p>Java:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">Person</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="kd">private</span><span class="w"> </span><span class="kd">final</span><span class="w"> </span><span class="n">String</span><span class="w"> </span><span class="n">name</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="kd">public</span><span class="w"> </span><span class="nf">Person</span><span class="p">(</span><span class="n">String</span><span class="w"> </span><span class="n">name</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">this</span><span class="p">.</span><span class="na">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">name</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Kotlin:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Person</span><span class="p">(</span><span class="k">val</span> <span class="py">name</span><span class="p">:</span> <span class="n">String</span><span class="p">)</span>
</span></span></code></pre></div><p>The benefits of using Kotlin immediately start showing! But let&rsquo;s go over this in a sysmetatic fashion and break down each aspect of what makes Kotlin so great.</p>
<h2 id="constructors-and-parameters">Constructors and parameters</h2>
<p>Kotlin uses a very compact syntax for describing primary constructors. With some clever tricks around default values, we can create many constructors out of a single one!</p>
<p>Notice the <code>val</code> in the parameter name. It&rsquo;s a concise syntax for declaring variables and initializing them from the constructor itself. Like any other property, they can be mutable (<code>var</code>) or immutable (<code>val</code>). If you remove the <code>val</code> in our <code>Person</code> constructor, you will not have a <code>name</code> variable available on its instance, i.e., <code>Person(&quot;Person 1&quot;).name</code> will not resolve.</p>
<p>The primary constructor cannot have any code so Kotlin provides something called &lsquo;initializer blocks&rsquo; to allow you to run initialization code from your constructor. Try running the code below in the <a href="https://play.kotlinlang.org/">Kotlin playground</a></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Person</span><span class="p">(</span><span class="k">val</span> <span class="py">name</span><span class="p">:</span> <span class="n">String</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">init</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">println</span><span class="p">(</span><span class="s2">&#34;Invoking constructor!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">val</span> <span class="py">_unused</span> <span class="p">=</span> <span class="n">Person</span><span class="p">(</span><span class="s2">&#34;Matt&#34;</span><span class="p">)</span>
</span></span></code></pre></div><p>Moving on, let&rsquo;s add an optional age parameter to our classes, with a default value of 18. To make it convenient to see how different constructors affect values, we&rsquo;re also including an implementation of the <code>toString</code> method for some classing print debugging.</p>
<p>Java:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">Person</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="kd">private</span><span class="w"> </span><span class="kd">final</span><span class="w"> </span><span class="n">String</span><span class="w"> </span><span class="n">name</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="kd">private</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="n">age</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">18</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="kd">public</span><span class="w"> </span><span class="nf">Person</span><span class="p">(</span><span class="n">String</span><span class="w"> </span><span class="n">name</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">this</span><span class="p">.</span><span class="na">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">name</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="kd">public</span><span class="w"> </span><span class="nf">Person</span><span class="p">(</span><span class="n">String</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="n">age</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">this</span><span class="p">(</span><span class="n">name</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">this</span><span class="p">.</span><span class="na">age</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">age</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nd">@Override</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="kd">public</span><span class="w"> </span><span class="n">String</span><span class="w"> </span><span class="nf">toString</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="s">&#34;Name=&#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s">&#34;,age=&#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">Integer</span><span class="p">.</span><span class="na">toString</span><span class="p">(</span><span class="n">age</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Kotlin:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Person</span><span class="p">(</span><span class="k">val</span> <span class="py">name</span><span class="p">:</span> <span class="n">String</span><span class="p">,</span> <span class="k">val</span> <span class="py">age</span><span class="p">:</span> <span class="n">Int</span> <span class="p">=</span> <span class="mi">18</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">override</span> <span class="k">fun</span> <span class="nf">toString</span><span class="p">()</span> <span class="p">:</span> <span class="n">String</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// I&#39;ll go over string templates in a future post, hold me to it :)
</span></span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="s2">&#34;Name=</span><span class="si">$name</span><span class="s2">,age=</span><span class="si">$age</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Lots of new things here! Let&rsquo;s break them down.</p>
<p>Kotlin has a feature called &lsquo;default parameters&rsquo;, that allows you to specify default values for parameters, thus making them optional when creating an instance of the class.</p>
<p>An important note here is that constructors with default values don&rsquo;t directly work with Java if you&rsquo;re writing a library or any code that would require to interop with Java. Use the Kotlin <code>@JvmOverloads</code> annotation to handle that for you.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Person</span> <span class="nd">@JvmOverloads</span> <span class="k">constructor</span><span class="p">(</span><span class="k">val</span> <span class="py">name</span><span class="p">:</span> <span class="n">String</span><span class="p">,</span> <span class="k">val</span> <span class="py">age</span><span class="p">:</span> <span class="n">Int</span> <span class="p">=</span> <span class="mi">18</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">override</span> <span class="k">fun</span> <span class="nf">toString</span><span class="p">()</span> <span class="p">:</span> <span class="n">String</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="s2">&#34;Name=</span><span class="si">$name</span><span class="s2">,age=</span><span class="si">$age</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Doing this will generate constructors similar to how we previously wrote in Java, to allow both Kotlin and Java callers to work.</p>
<h2 id="finality-of-classes">Finality of classes</h2>
<p>In Kotlin, all classes are final by default, and cannot be inherited while Java defaults to extensible classes. The <code>open</code> keyword marks Kotlin classes as extensible, and the <code>final</code> keyword does the opposite on Java.</p>
<p>Java:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="kd">class</span> <span class="nc">Man</span><span class="w"> </span><span class="kd">extends</span><span class="w"> </span><span class="n">Person</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="cm">/* Class body */</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c1">// Valid in Java</span><span class="w">
</span></span></span></code></pre></div><p>Kotlin:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Man</span><span class="p">(</span><span class="k">val</span> <span class="py">firstName</span><span class="p">:</span> <span class="n">String</span><span class="p">)</span> <span class="p">:</span> <span class="n">Person</span><span class="p">(</span><span class="n">firstName</span><span class="p">)</span> <span class="c1">// Errors!
</span></span></span></code></pre></div><p>Trying it out in the Kotlin REPL</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="p">&gt;</span> <span class="p">&gt;</span> <span class="p">&gt;</span> <span class="k">class</span> <span class="nc">Person</span> <span class="nd">@JvmOverloads</span> <span class="k">constructor</span><span class="p">(</span><span class="k">val</span> <span class="py">name</span><span class="p">:</span> <span class="n">String</span><span class="p">,</span> <span class="k">val</span> <span class="py">age</span><span class="p">:</span> <span class="n">Int</span> <span class="p">=</span> <span class="mi">18</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"><span class="p">&gt;</span> <span class="p">&gt;</span> <span class="p">&gt;</span> <span class="o">..</span><span class="p">.</span> <span class="k">override</span> <span class="k">fun</span> <span class="nf">toString</span><span class="p">()</span> <span class="p">:</span> <span class="n">String</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"><span class="p">&gt;</span> <span class="p">&gt;</span> <span class="p">&gt;</span> <span class="o">..</span><span class="p">.</span>   <span class="k">return</span> <span class="s2">&#34;Name=</span><span class="si">$name</span><span class="s2">,age=</span><span class="si">$age</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">&gt;</span> <span class="p">&gt;</span> <span class="p">&gt;</span> <span class="o">..</span><span class="p">.</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">&gt;</span> <span class="p">&gt;</span> <span class="p">&gt;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">&gt;</span> <span class="p">&gt;</span> <span class="p">&gt;</span> <span class="k">class</span> <span class="nc">Man</span><span class="p">(</span><span class="k">val</span> <span class="py">firstName</span><span class="p">:</span> <span class="n">String</span><span class="p">)</span> <span class="p">:</span> <span class="n">Person</span><span class="p">(</span><span class="n">firstName</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">&gt;</span> <span class="p">&gt;</span> <span class="p">&gt;</span> <span class="n">error</span><span class="p">:</span> <span class="k">this</span> <span class="n">type</span> <span class="k">is</span> <span class="k">final</span><span class="p">,</span> <span class="n">so</span> <span class="k">it</span> <span class="n">cannot</span> <span class="n">be</span> <span class="n">inherited</span> <span class="n">from</span>
</span></span><span class="line"><span class="cl"><span class="p">&gt;</span> <span class="p">&gt;</span> <span class="p">&gt;</span> <span class="k">class</span> <span class="nc">Man</span><span class="p">(</span><span class="k">val</span> <span class="py">firstName</span><span class="p">:</span> <span class="n">String</span><span class="p">)</span> <span class="p">:</span> <span class="n">Person</span><span class="p">(</span><span class="n">firstName</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                                         <span class="p">^</span>
</span></span></code></pre></div><p>Makes sense, since that&rsquo;s default for Kotlin. Let&rsquo;s add the <code>open</code> keyword to our definition of <code>Person</code> and try again.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="p">&gt;</span> <span class="p">&gt;</span> <span class="p">&gt;</span> <span class="k">open</span> <span class="k">class</span> <span class="nc">Person</span> <span class="nd">@JvmOverloads</span> <span class="k">constructor</span><span class="p">(</span><span class="k">val</span> <span class="py">name</span><span class="p">:</span> <span class="n">String</span><span class="p">,</span> <span class="k">val</span> <span class="py">age</span><span class="p">:</span> <span class="n">Int</span> <span class="p">=</span> <span class="mi">18</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"><span class="p">&gt;</span> <span class="p">&gt;</span> <span class="p">&gt;</span> <span class="o">..</span><span class="p">.</span> <span class="k">override</span> <span class="k">fun</span> <span class="nf">toString</span><span class="p">()</span> <span class="p">:</span> <span class="n">String</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"><span class="p">&gt;</span> <span class="p">&gt;</span> <span class="p">&gt;</span> <span class="o">..</span><span class="p">.</span>   <span class="k">return</span> <span class="s2">&#34;Name=</span><span class="si">$name</span><span class="s2">,age=</span><span class="si">$age</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">&gt;</span> <span class="p">&gt;</span> <span class="p">&gt;</span> <span class="o">..</span><span class="p">.</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">&gt;</span> <span class="p">&gt;</span> <span class="p">&gt;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">&gt;</span> <span class="p">&gt;</span> <span class="p">&gt;</span> <span class="k">class</span> <span class="nc">Man</span><span class="p">(</span><span class="k">val</span> <span class="py">firstName</span><span class="p">:</span> <span class="n">String</span><span class="p">)</span> <span class="p">:</span> <span class="n">Person</span><span class="p">(</span><span class="n">firstName</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">&gt;</span> <span class="p">&gt;</span> <span class="p">&gt;</span> <span class="n">println</span><span class="p">(</span><span class="n">Man</span><span class="p">(</span><span class="s2">&#34;Henry&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="p">&gt;</span> <span class="p">&gt;</span> <span class="p">&gt;</span> <span class="n">Name</span><span class="p">=</span><span class="n">Henry</span><span class="p">,</span><span class="n">age</span><span class="p">=</span><span class="mi">18</span>
</span></span></code></pre></div><p>And everything works as we&rsquo;d expect it to. This is a behavior change that is confusing and undesirable to a lot of people, so Kotlin provides a compiler plugin to mark all classes as <code>open</code> by default. Check out the <a href="https://kotlinlang.org/docs/reference/compiler-plugins.html#all-open-compiler-plugin"><code>kotlin-allopen</code></a> page for more information about how to configure the plugin for your needs.</p>
<h2 id="static-utils-classes">Static utils classes</h2>
<p>Everybody knows that you don&rsquo;t have a real project until you have a <code>StringUtils</code> class. Usually it&rsquo;d be a <code>public static final</code> class with a bunch of static methods. While Kotlin has a sweeter option of <a href="https://kotlinlang.org/docs/tutorials/kotlin-for-py/extension-functionsproperties.html">extension functions and properties</a>, for purposes of comparison we&rsquo;ll stick with the old Java way of doing things.</p>
<p>Here&rsquo;s a small function I use to convert Android&rsquo;s URI paths to human-readable versions.</p>
<p>Java:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="kd">static</span><span class="w"> </span><span class="kd">final</span><span class="w"> </span><span class="kd">class</span> <span class="nc">StringUtils</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="kd">public</span><span class="w"> </span><span class="kd">static</span><span class="w"> </span><span class="n">String</span><span class="w"> </span><span class="nf">normalizePath</span><span class="p">(</span><span class="kd">final</span><span class="w"> </span><span class="n">String</span><span class="w"> </span><span class="n">str</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">str</span><span class="p">.</span><span class="na">replace</span><span class="p">(</span><span class="s">&#34;/document/primary:&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;/sdcard/&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Kotlin:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="cl"><span class="k">object</span> <span class="nc">StringUtils</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// I&#39;ll cover this declaration style too. It&#39;s just the first post!
</span></span></span><span class="line"><span class="cl">  <span class="k">fun</span> <span class="nf">normalizePath</span><span class="p">(</span><span class="n">str</span><span class="p">:</span> <span class="n">String</span><span class="p">):</span> <span class="n">String</span> <span class="p">=</span> <span class="n">str</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&#34;/document/primary:&#34;</span><span class="p">,</span> <span class="s2">&#34;/sdcard/&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>A recurring pattern with Kotlin is concise code, as you can see in this case.</p>
<p>That&rsquo;s all for this one! Let me know in the comments about what you&rsquo;d prefer to be next week&rsquo;s post about or if you feel I missed something in this one and I&rsquo;ll definitely try to make it happen :)</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>#TeachingKotlin - Kotlin for Android Java developers</title>
      <link>https://msfjarvis.dev/posts/teaching-kotlin--kotlin-for-android-java-developers/</link>
      <pubDate>Fri, 20 Sep 2019 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/teaching-kotlin--kotlin-for-android-java-developers/</guid>
      <description>Kotlin&amp;rsquo;s been great for me &amp;ndash; and millions others, as evident by its explosive growth. Long-time Java developers may feel hesitant to give it a shot. This series aims to smoothen this transition, letting people know what benefits they might reap from Kotlin, and what differences should they be careful about.</description>
      <content:encoded><![CDATA[<p>Anybody familiar with my work knows that I am a fan of the <a href="https://kotlinlang.org/" title="Kotlin">Kotlin</a> programming language, especially it&rsquo;s interoperability with Java with respect to Android. I&rsquo;ll admit, I&rsquo;ve not been a fan since day one. The abundant lambdas worried me and everything being that much shorter to implement was confusing to a person whose first real programming task was in the Java programming language.</p>
<p>As I leaped over the initial hurdle of hesitation and really got into Kotlin, I was mindblown. Everything is so much better! Being able to break away from Java&rsquo;s explicit verbosity into letting the language do things for you is a bit daunting at first but over time you&rsquo;ll come to appreciate the time you save and in turn how many potential problems you can avoid by simply not having to do everything yourself. <a href="https://github.com/kelseyhightower/nocode">Can&rsquo;t have bugs if you don&rsquo;t write code</a> :p</p>
<p>As I&rsquo;ve gotten more and more into the Kotlin ecosystem and community and converted developers into adopting Kotlin, into taking that first step, I&rsquo;ve realised most of them have a common set of concerns and often a lack of knowledge about what Kotlin actually brings to the table and what are the drawbacks of using a &ldquo;new&rdquo; language over an established behemoth like Java.</p>
<p>Hence I&rsquo;ve decided to publish a series of posts outlining exactly that &ndash; What to expect when moving to Kotlin from Java, the benefits and the common pitfalls as well as current limitations that may or may not hinder said move. The first post of the series will go up on the upcoming Monday evening 6:00 PM IST (Indian Standard Time), and all following ones will be published at the same time every week. I&rsquo;d like to keep this up for as long as possible and so I&rsquo;m not declaring this as a <code>n</code>-part series right off the bat. We&rsquo;ll figure it out as we go :)</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Tools for effective Rust development</title>
      <link>https://msfjarvis.dev/posts/tools-for-effective-rust-development/</link>
      <pubDate>Sat, 07 Sep 2019 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/tools-for-effective-rust-development/</guid>
      <description>Rust is an amazing systems language that is on an explosive rise thanks to its memory safety guarantees and fast, iterative development. In this post, I recap some of the tooling that I use with Rust to make coding in it even more fun and intuitive</description>
      <content:encoded><![CDATA[<p><a href="https://rust-lang.org/">Rust</a> is a memory-safe systems language that is blazing fast, and comes with no runtime or garbage collector overhead. It can be used to build very performant web services, CLI tools, and even <a href="https://github.com/fishinabarrel/linux-kernel-module-rust">Linux kernel modules</a>!</p>
<p><a href="https://rust-lang.org/">Rust</a> also provides an assortment of tools to make development faster and more user-friendly. I&rsquo;ll be going over some of them here that I&rsquo;ve personally used and found to be amazing.</p>
<h2 id="cargo-edit">cargo-edit</h2>
<p><a href="https://github.com/killercup/cargo-edit">cargo-edit</a> is a crate that extends Rust&rsquo;s Cargo tool with <code>add</code>, <code>remove</code> and <code>upgrade</code> commands that allow you to manage dependencies with ease. The <a href="https://github.com/killercup/cargo-edit/blob/master/README.md#available-subcommands">documentation</a> goes over these options in detail.</p>
<p>I personally find <code>cargo-edit</code> useful in projects with a lot of dependencies as it gets tiresome to manually hunt down updated versions.</p>
<h2 id="cargo-clippy">cargo-clippy</h2>
<p><a href="https://github.com/rust-lang/rust-clippy">cargo-clippy</a> is an advanced linter for Rust that brings together <strong>331</strong> (<a href="https://rust-lang.github.io/rust-clippy/stable/index.html">at the time of writing</a>) different lints in one package that&rsquo;s built and maintained by the Rust team.</p>
<p>I&rsquo;ve found it to be a great help alongside the official documentation and <a href="https://doc.rust-lang.org/book/">&ldquo;the book&rdquo;</a> as a way of writing cleaner and more efficient Rust code. As a beginner Rustacean, I find it very helpful in breaking away from my patterns from other languages and using more &ldquo;rust-y&rdquo; constructs and expressions in my code.</p>
<h2 id="rustfmt">rustfmt</h2>
<p><a href="https://github.com/rust-lang/rustfmt">rustfmt</a> is the official formatting tool for Rust code. It&rsquo;s an opinionated, zero-configuration tool that &ldquo;just works&rdquo;. It has not reached a <code>1.0</code> release yet, which entails some <a href="https://github.com/rust-lang/rustfmt#limitations">caveats</a> with its usage but in my experience it will work for most people and codebases without any hassle.</p>
<p>As a Kotlin programmer I am very used to having an official styleguide for consistent formatting across all projects. <code>rustfmt</code> brings that same convenience to Rust development, which is major since Rust does not have any official IDE which would do it automatically.</p>
<h2 id="rls">rls</h2>
<p><a href="https://github.com/rust-lang/rls">rls</a> is Rust&rsquo;s implementation of Microsoft&rsquo;s <a href="https://microsoft.github.io/language-server-protocol/">language-server-protocol</a>, an attempt at standardizing the interface between language tooling and IDEs to allow things like code completion, find all references and documentation on hover to work seamlessly across different IDEs. <a href="https://code.visualstudio.com/">VSCode</a> implements the <code>language-server-protocol</code> and integrates seamlessly with <code>rls</code> using the <a href="https://marketplace.visualstudio.com/items?itemName=rust-lang.rust">rust-lang.rust</a> extension to create a compelling IDE experience.</p>
<p>Being a beginner, the ability for code to be checked within the editor and not requiring builds for each change is a huge speed-up in the learning and development process. Documentation about crates and errors being available directly on hover is certainly helpful in furthering my knowledge and understanding of the language.</p>
<h2 id="conclusion">Conclusion</h2>
<p>So this is my list of must-have tooling that has helped me continuously improve as a Rustacean. I&rsquo;m VERY curious to hear what others are using! I opted to stick with official tools where possible since they&rsquo;ve proven very reliable and I seem to find considerably more help online with them, but I&rsquo;d love to try out non-official alternatives that offer significant benefits :)</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>How to get involved in open source</title>
      <link>https://msfjarvis.dev/posts/how-to-get-involved-in-open-source/</link>
      <pubDate>Fri, 31 May 2019 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/how-to-get-involved-in-open-source/</guid>
      <description>Starting with OSS can be daunting for many. Here&amp;rsquo;s a recap of my experiences with OSS with some tips on how to get started yourself.</description>
      <content:encoded><![CDATA[<p>The most common question I get when I recommend open source as a launching pad for budding developers is &ldquo;Where do I start?&rdquo;.</p>
<p>The answer: <em>anywhere!</em></p>
<p>There&rsquo;s a plethora of open source software out there, and not everybody needs to have an encyclopaedic knowledge of the codebase to contribute. You can contribute small things like <a href="https://github.com/portainer/portainer/commit/173c673d37ea2e4bb82d159b601e60109a435601">fixing dead links in the README</a> to <a href="https://github.com/mozilla-mobile/fenix/commits/master?author=msfjarvis">resolving trivial compilation warnings</a> to simply <a href="https://github.com/opengapps/opengapps/commits/master/.github/ISSUE_TEMPLATE.md">tweaking an issue template</a>.</p>
<p>The reason I&rsquo;m linking my own commits is because I want to let people know that the guy helming a <a href="https://github.com/substratum">theme engine</a> is also out of his element at times and there&rsquo;s no shame in admitting it :)</p>
<p>Thanks to the adoption of specs like <a href="https://allcontributors.org">all-contributors</a>, OSS is more friendly and welcoming than ever. Contribute literally <strong>anything</strong> to a project you use and scale up from there.</p>
<p>Remember: You <em>will</em> make mistakes in the process. Don&rsquo;t give up! There&rsquo;s always a project looking for any kind of help it can get. Start your search at home &ndash; See what apps and desktop software you use that&rsquo;s open source, and if that&rsquo;s something you&rsquo;d like to give back to or even fix something in, even if it&rsquo;s driven by the need to enhance your experience than your goodwill. Linus Torvalds, the creator of Linux <a href="https://www.bbc.com/news/technology-18419231">famously said</a> this:</p>
<blockquote>
<p>I do not see open source as some big goody-goody &ldquo;let&rsquo;s all sing kumbaya around the campfire and make the world a better place&rdquo;. No, open source only really works if everybody is contributing for their own selfish reasons.</p>
</blockquote>
<p>And it&rsquo;s true! Most apps I contribute to right now, like <a href="https://github.com/AdAway/AdAway">AdAway</a> and <a href="https://github.com/zeapo/Android-Password-Store">Android Password Store</a>, began as a manifestation of personal annoyance. I found things to be lacking, and decided to address it. In the end that benefitted both me and the project.</p>
<p>In conclusion, I&rsquo;d like to reiterate this &ndash; Contributing <strong>anything</strong> is contributing!</p>
<p>P.S. It&rsquo;s okay to be nervous about it. I spent two weeks researching SSL before submitting a simple null check to Google&rsquo;s <a href="https://github.com/google/conscrypt/pull/471">conscrypt</a> library :P</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>I&#39;m gonna blog!</title>
      <link>https://msfjarvis.dev/posts/i-m-gonna-blog/</link>
      <pubDate>Thu, 30 May 2019 12:00:00 +0530</pubDate>
      <author>me@msfjarvis.dev (Harsh Shandilya)</author>
      <guid>https://msfjarvis.dev/posts/i-m-gonna-blog/</guid>
      <description>&lt;p&gt;With all my involvement in OSS development around Android, I come across a lot of new things on the daily. This blog will hopefully serve as a index for those findings, and an excuse for me to properly research and document them for myself and others.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>With all my involvement in OSS development around Android, I come across a lot of new things on the daily. This blog will hopefully serve as a index for those findings, and an excuse for me to properly research and document them for myself and others.</p>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
