<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Paweł Pokrywka's Lab: Random]]></title><description><![CDATA[A mix of miscellaneous IT stuff I’ve worked on.]]></description><link>https://www.pawelpokrywka.com/s/random</link><image><url>https://substackcdn.com/image/fetch/$s_!gJuv!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe44a7fc6-d5ec-4a99-984a-7a7e0bc80a17_800x800.png</url><title>Paweł Pokrywka&apos;s Lab: Random</title><link>https://www.pawelpokrywka.com/s/random</link></image><generator>Substack</generator><lastBuildDate>Fri, 01 May 2026 19:20:05 GMT</lastBuildDate><atom:link href="https://www.pawelpokrywka.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Paweł Pokrywka]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[rss1@pawelpokrywka.com]]></webMaster><itunes:owner><itunes:email><![CDATA[rss1@pawelpokrywka.com]]></itunes:email><itunes:name><![CDATA[Paweł Pokrywka]]></itunes:name></itunes:owner><itunes:author><![CDATA[Paweł Pokrywka]]></itunes:author><googleplay:owner><![CDATA[rss1@pawelpokrywka.com]]></googleplay:owner><googleplay:email><![CDATA[rss1@pawelpokrywka.com]]></googleplay:email><googleplay:author><![CDATA[Paweł Pokrywka]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Create a Ruby gem with Zeitwerk as a development-only dependency (tutorial)]]></title><description><![CDATA[Forget require statements and make your gem lightweight at the same time]]></description><link>https://www.pawelpokrywka.com/p/gem-with-zeitwerk-as-development-only-dependency</link><guid isPermaLink="false">https://www.pawelpokrywka.com/p/gem-with-zeitwerk-as-development-only-dependency</guid><dc:creator><![CDATA[Paweł Pokrywka]]></dc:creator><pubDate>Sun, 20 Aug 2023 17:56:54 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!MQCJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!MQCJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MQCJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg 424w, https://substackcdn.com/image/fetch/$s_!MQCJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg 848w, https://substackcdn.com/image/fetch/$s_!MQCJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!MQCJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!MQCJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg" width="1456" height="942" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:942,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:712764,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!MQCJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg 424w, https://substackcdn.com/image/fetch/$s_!MQCJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg 848w, https://substackcdn.com/image/fetch/$s_!MQCJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!MQCJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Image by <a href="https://pixabay.com/users/alexas_fotos-686414/?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=3223739">Alexa</a> from <a href="https://pixabay.com//?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=3223739">Pixabay</a></figcaption></figure></div><h2>For the impatient</h2><p>If you know what Zeitwerk is, understand the problem, and just want to do what the title of this post promises, <a href="https://www.pawelpokrywka.com/i/136189156/have-your-cake-and-eat-it-too">jump straight into the tutorial</a>.</p><p>Otherwise, keep on reading.</p><h2>The problem</h2><p>If you work on a non-Rails Ruby project, managing code loading may be challenging. Following the <a href="https://en.wikipedia.org/wiki/Single-responsibility_principle">Single Responsibility Principle</a> means having many specialized classes instead of one doing all the work. And if you keep each class in a separate file you quickly end up with a bunch of files that need to be loaded. That&#8217;s a lot of sad <em>require</em> statements hiding in your files. You can almost hear their mischievous giggles when you rename a class or move it into a different namespace. It may sound similar to:</p><pre><code><code>cannot load such file -- my_perfect_class (LoadError)</code></code></pre><p>Those exceptions make me angry. And Ruby was meant to be optimized<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> for the programmer&#8217;s happiness, right?</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qXvr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qXvr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg 424w, https://substackcdn.com/image/fetch/$s_!qXvr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg 848w, https://substackcdn.com/image/fetch/$s_!qXvr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!qXvr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qXvr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg" width="1456" height="1166" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1166,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2254571,&quot;alt&quot;:&quot;Classic oil painting of a sleeping woman haunted by a Ruby LoadError nightmare. A puzzled Ruby programmer in the background.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Classic oil painting of a sleeping woman haunted by a Ruby LoadError nightmare. A puzzled Ruby programmer in the background." title="Classic oil painting of a sleeping woman haunted by a Ruby LoadError nightmare. A puzzled Ruby programmer in the background." srcset="https://substackcdn.com/image/fetch/$s_!qXvr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg 424w, https://substackcdn.com/image/fetch/$s_!qXvr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg 848w, https://substackcdn.com/image/fetch/$s_!qXvr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!qXvr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Inspired by <em>The Nightmare</em> (1791) by Henry Fuseli, oil on canvas 101.6 &#215; 126.7 cm, Detroit Institute of Arts</figcaption></figure></div><h2>Enter Zeitwerk</h2><p>Well, there are solutions. One of the best, in my opinion, is <a href="https://github.com/fxn/zeitwerk">Zeitwerk</a>, described as an &#8220;Efficient and thread-safe code loader for Ruby&#8221;.</p><p>You probably heard about it, as it is used by Rails and Hanami. Zeitwerk is the reason you don&#8217;t need to worry about <em>require</em> statements when working with Rails apps. Just follow the intuitive convention of naming files the same as the classes they contain.</p><h2>Using Zeitwerk in your own gem</h2><p>The good news is: Zeitwerk is not limited to Rails. You can <a href="https://www.akshaykhot.com/using-zeitwerk-outside-rails/">use it in any Ruby project</a>, including any gem you create.</p><p>Just put it into Gemspec, and <a href="https://github.com/fxn/zeitwerk#for_gem">add a few lines</a> of code. That&#8217;s it, you are now free to focus on features instead of code loading.</p><p>The bad news is: your gem now depends on Zeitwerk. This means each project using your gem depends on it too. Some people may not like it and decide to not use your awesome gem!</p><p>That&#8217;s because it&#8217;s generally a good practice to minimize dependencies. The benefits include security, maintainability, and stability.</p><p>Does it mean you have to choose between ease of development and dependency minimalization?</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mXnM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4ee9127-d575-4854-8717-bb27487268e8_1024x684.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mXnM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4ee9127-d575-4854-8717-bb27487268e8_1024x684.jpeg 424w, https://substackcdn.com/image/fetch/$s_!mXnM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4ee9127-d575-4854-8717-bb27487268e8_1024x684.jpeg 848w, https://substackcdn.com/image/fetch/$s_!mXnM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4ee9127-d575-4854-8717-bb27487268e8_1024x684.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!mXnM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4ee9127-d575-4854-8717-bb27487268e8_1024x684.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mXnM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4ee9127-d575-4854-8717-bb27487268e8_1024x684.jpeg" width="1024" height="684" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e4ee9127-d575-4854-8717-bb27487268e8_1024x684.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:684,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:207730,&quot;alt&quot;:&quot;Classic painting depicting a Ruby developer as Heracles having to choose between two females. The woman on the left symbolizes fewer dependencies. The female on the right symbolizes less frustration.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Classic painting depicting a Ruby developer as Heracles having to choose between two females. The woman on the left symbolizes fewer dependencies. The female on the right symbolizes less frustration." title="Classic painting depicting a Ruby developer as Heracles having to choose between two females. The woman on the left symbolizes fewer dependencies. The female on the right symbolizes less frustration." srcset="https://substackcdn.com/image/fetch/$s_!mXnM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4ee9127-d575-4854-8717-bb27487268e8_1024x684.jpeg 424w, https://substackcdn.com/image/fetch/$s_!mXnM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4ee9127-d575-4854-8717-bb27487268e8_1024x684.jpeg 848w, https://substackcdn.com/image/fetch/$s_!mXnM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4ee9127-d575-4854-8717-bb27487268e8_1024x684.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!mXnM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4ee9127-d575-4854-8717-bb27487268e8_1024x684.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Inspired by <em>The Choice of Heracles</em> (1596) by Annibale Carracci, oil on canvas 239 cm &#215; 165 cm, National Museum of Capodimonte</figcaption></figure></div><h2>Have your cake and eat it too</h2><div class="pullquote"><p>Can you use Zeitwerk for development, but not include it as a runtime gem dependency?</p></div><p>I asked myself this question while working on <a href="https://www.pawelpokrywka.com/p/cryptreboot">cryptreboot</a>, a gem that allows a machine with an encrypted disk to reboot by <strong>requesting a passphrase beforehand, rather than after the reboot.</strong></p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;ad909c9d-ad12-4c98-ad2d-5021f0b5ae43&quot;,&quot;caption&quot;:&quot;As an Ethereum solo staker, I want my Linux staking box to be as secure as feasible. It applies to physical security too. If the attacker gains access to the validator key stored on disk, it will allow him/her to do malicious things such as intentionally exposing the validator to the slashing penalties.&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Rebooting Linux with encrypted disk&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:41077879,&quot;name&quot;:&quot;Pawe&#322; Pokrywka&quot;,&quot;bio&quot;:&quot;I write about security, privacy, and complex systems&#8212;exploring them from the messy middle ground between engineering and research.&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1c35edc2-abb7-4761-8eb4-f379f25e519a_800x800.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2023-07-31T13:53:24.456Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60692078-4e29-4484-9988-f262d39b153f_4608x3456.jpeg&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.pawelpokrywka.com/p/rebooting-linux-with-encrypted-disk&quot;,&quot;section_name&quot;:&quot;Privacy &amp; Security&quot;,&quot;video_upload_id&quot;:null,&quot;id&quot;:134149533,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:0,&quot;comment_count&quot;:0,&quot;publication_id&quot;:null,&quot;publication_name&quot;:&quot;Pawe&#322; Pokrywka's Lab&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe44a7fc6-d5ec-4a99-984a-7a7e0bc80a17_800x800.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><p>I was not able to find an answer on Google. ChatGPT also failed. Therefore I decided to do it by myself.</p><p>In this tutorial, I will show you how to create a gem from the ground up. It will use Zeitwerk in development, while not depending on it in runtime. And it will still work.</p><h2>Conventions</h2><p>The first character of a line in code blocks has a special meaning:</p><ul><li><p>$ means the remaining part should be executed in the shell,</p></li><li><p>&gt; means the remaining part should be executed in IRB.</p></li></ul><p>In other cases, the block contains the actual code or result of an action you performed.</p><p>I assume you use a Unix-based OS such as Linux distribution (may be run in WSL), *BSD, or macOS. If you run Windows, you may need to adjust some shell commands.</p><h2>Start with an empty gem</h2><p>We will call our gem <em>zeitgeist</em>. Execute in the console:</p><pre><code><code>$ bundle gem zeitgeist
$ cd zeitgeist</code></code></pre><p>Now adjust the Gemspec to make the gem buildable. Open <em>zeitgeist.gemspec</em> file and:</p><ol><li><p>Fill <em>summary</em>, <em>homepage</em>, <em>source_code_uri</em>, and <em>changelog_uri</em>.</p></li><li><p>Delete the line containing the <em>description</em>.</p></li></ol><p>The results should look similar to this (I stripped comments):</p><pre><code><code>require_relative "lib/zeitgeist/version"

Gem::Specification.new do |spec|
  spec.name = "zeitgeist"
  spec.version = Zeitgeist::VERSION
  spec.authors = ["Pawel"]
  spec.email = ["pepawel@users.noreply.github.com"]

  spec.summary = "Awesome gem using Zeitwerk only for development."
  spec.homepage = "https://github.com/pepawel/zeitgeist"
  spec.license = "MIT"
  spec.required_ruby_version = "&gt;= 2.6.0"

  spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"

  spec.metadata["homepage_uri"] = spec.homepage
  spec.metadata["source_code_uri"] = spec.homepage
  spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"

  spec.files = Dir.chdir(__dir__) do
    `git ls-files -z`.split("\x0").reject do |f|
      (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
    end
  end
  spec.bindir = "exe"
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
  spec.require_paths = ["lib"]
end</code></code></pre><p>You should spend some more time on Gemspec if you work on a real gem. The above example minimizes changes needed for building, but it&#8217;s not production-ready.</p><p>Install dependencies:</p><pre><code><code>$ bundle config set --local path .bundle/gems # for easy cleanup
$ bundle install</code></code></pre><p>Let&#8217;s commit the changes and check if you can build the gem:</p><pre><code><code>$ git add .
$ git commit -m 'Initial commit'
$ rake build</code></code></pre><p>It should succeed. However, if there is an error it should be self-explanatory. Most probably you will need to adjust Gemspec.</p><h2>Add some logic</h2><p>Place the following code in <em>lib/zeitgeist/programmer.rb</em>:</p><pre><code><code>module Zeitgeist
  class Programmer
    def happy?
      # Ruby was created in 1993
      Time.now.year &gt;= 1993 ? 'yes' : 'no'
    end
  end
end</code></code></pre><p>To check if it works, run IRB in the context of the gem by executing in the terminal:</p><pre><code><code>$ bin/console</code></code></pre><p>When you try to access <em>Zeitgeist::Programmer</em>, you will see it&#8217;s not loaded:</p><pre><code><code>&gt; Zeitgeist::Programmer
(irb):1:in `&lt;main&gt;':
  uninitialized constant Zeitgeist::Programmer (NameError)
  from bin/console:15:in `&lt;main&gt;'</code></code></pre><p>To make it work, you will need to manually require the correct file:</p><pre><code><code>&gt; require 'zeitgeist/programmer'
=&gt; true
&gt; Zeitgeist::Programmer
=&gt; Zeitgeist::Programmer</code></code></pre><p>But we want it to be required automatically by Zeitwerk. In our example above, it is enough to simply <em>require</em> the correct file. But with your codebase becoming larger, the benefits of using auto-loader will become more and more visible.</p><h2>Enable Zeitwerk</h2><p>Add the Zeitwerk to your Gemfile and install it:</p><pre><code><code>$ bundle add zeitwerk</code></code></pre><p>Gems added to the gem&#8217;s Gemfile are available in development only, so Zeitwerk won&#8217;t become a runtime dependency.</p><p>The next step is to make sure your code uses Zeitwerk. Adjust <em>lib/zeitgeist.rb</em> to include the following lines before the <em>Zeitgeist</em> module definition:</p><pre><code><code>require "zeitwerk"
loader = Zeitwerk::Loader.for_gem
loader.setup</code></code></pre><p>Now run the console again and check if your code is being auto-loaded:</p><pre><code><code>$ bin/console
&gt; Zeitgeist::Programmer.new.happy?
=&gt; "yes"</code></code></pre><p>It works, so we can commit our code changes:</p><pre><code><code>$ git add lib Gemfile*
$ git commit -m 'Add business logic and setup autoloader'</code></code></pre><p>However, it won&#8217;t work when installed on a system without Zeitwerk. The gem will install normally but will raise an exception on first use because it silently depends on Zeitwerk.</p><h2>Basic loader</h2><p>We need a lightweight loader for runtime. Its task would be to load all the Ruby files in gem&#8217;s <em>lib/</em> directory. Why not loop over the files and simply <em>require</em> each one? It&#8217;s because the order of loading files matters. In the loop approach, we may end up loading file <em>B</em> depending on file <em>A</em> before file <em>A</em> is loaded.</p><p>To make sure we use the correct loading order, let&#8217;s create the loader file manually in <em>lib/basic_loader.rb</em>:</p><pre><code><code># Load every project file in one place
require 'zeitgeist/programmer'</code></code></pre><h2>Runtime vs development</h2><p>We want to use a basic loader for runtime and Zeitwerk for development. Therefore we need to distinguish between those two.</p><p>As <em>Gemfile</em> is used only in development, we could use it for this purpose. Add the following code to the beginning of that file:</p><pre><code><code>module ::Zeitgeist # :: is used to escape from Gemfile's scope
  AUTOLOADERS = []
end</code></code></pre><p>It simply sets a constant in the main module of our gem. It will be defined only in development.</p><p>You may notice it duplicates the module definition from the <em>zeitgeist.rb</em>. It&#8217;s perfectly fine as the modules in Ruby can be reopened as many times as you like.</p><p>Now, let&#8217;s adjust <em>lib/zeitgeist.rb</em> to decide which loader to use based on the constant we defined:</p><pre><code><code>require 'zeitgeist/version'

if defined? Zeitgeist::AUTOLOADERS
  require 'zeitwerk'
  Zeitgeist::AUTOLOADERS &lt;&lt; Zeitwerk::Loader.for_gem.tap do |loader|
    loader.ignore("#{__dir__}/basic_loader.rb")
    loader.setup
  end
else
  require 'basic_loader'
end

module Zeitgeist
  class Error &lt; StandardError; end
  # Your code goes here...
end</code></code></pre><p>The <em>basic_loaded.rb</em> file doesn&#8217;t match the Zeitwerk convention - it doesn&#8217;t define the <em>BasicLoader</em> constant. Therefore, to avoid warnings we add it to the ignored files as you see above.</p><p>The other change was to save the Zeitwerk instance into <em>AUTOLOADERS</em> constant. We will use it later.</p><p>Let&#8217;s commit our changes, build the gem, and install it locally:</p><pre><code><code>$ git add lib Gemfile
$ git commit -m 'Fix code loading'
$ rake build
$ gem install --user pkg/zeitgeist-0.1.0.gem</code></code></pre><p>As our gem doesn&#8217;t contain any executables, you can safely ignore the following warning if it appears:</p><pre><code><code>WARNING:  You don't have /home/user/.gem/ruby/3.0.0/bin in your PATH,
          gem executables will not run.</code></code></pre><p>Now we can test if the gem works:</p><pre><code><code>$ irb
&gt; require 'zeitgeist'
&gt; Zeitgeist::Programmer.new.happy?
=&gt; "yes"
&gt; defined? Zeitwerk
=&gt; nil</code></code></pre><p>It works, and the last line tells us Zeitwerk is not used.</p><p>Now let&#8217;s verify if development works too:</p><pre><code><code>$ bin/console
&gt; Zeitgeist::Programmer.new.happy?
=&gt; "yes"
&gt; defined? Zeitwerk
=&gt; "constant"</code></code></pre><p>It works too, but uses Zeitwerk just as we like.</p><p>However, we need to manually update <em>lib/basic_loader.rb</em> each time we add, remove, rename, or move the files. Why not automate it?</p><h2>Autogenerate basic loader</h2><p>Zeitwerk does <a href="https://medium.com/cedarcode/understanding-zeitwerk-in-rails-6-f168a9f09a1f">its magic</a> using <a href="https://www.rubydoc.info/stdlib/core/Module#autoload-instance_method">Module#autoload</a>. It guarantees the loading order is valid. We can use Zeitwerk to produce <em>lib/basic_loader.rb</em> file containing all the <em>require</em> statements in the correct order.</p><p>To generate that file, we will use Zeitwerk&#8217;s <em>on_load</em> callback which gets called every time file is loaded. We need to get a list of all files, therefore we will force Zeitwerk to load the entire codebase by using <em>#eager_load</em> method.</p><p>Put the following into <em>bin/loader</em>:</p><pre><code><code>#!/usr/bin/env ruby
# frozen_string_literal: true

require 'pathname'
require 'stringio'

class LoaderGenerator
  def call
    writer do |out|
      out.puts &lt;&lt;~HEADER
        # frozen_string_literal: true

        # File generated automatically, do not edit

      HEADER
      yield.each do |auto_loader|
        auto_loader.on_load do |_cpath, _value, abspath|
          next if abspath !~ /.rb$/i # skip directories

          out.puts "require '#{path_to_requirement(abspath)}'"
        end
        auto_loader.eager_load
      end
    end
    true
  end

  private

  def writer(&amp;block)
    output ? block.call(output) : File.open(loader_path, 'w', &amp;block)
  end

  def path_to_requirement(abspath)
    relative_path_from(loader_dir, abspath).sub(/.rb$/i, '')
  end

  def relative_path_from(base_dir, target_path)
    target = Pathname.new(target_path)
    base = Pathname.new(base_dir)
    target.relative_path_from(base).to_s
  end

  attr_reader :loader_path, :loader_dir, :output

  def initialize(loader_path, output = nil)
    @loader_path = loader_path
    @loader_dir = File.dirname(loader_path)
    @output = output
  end
end

class LoaderValidator
  def call(&amp;block)
    StringIO.new.tap do |buffer|
      LoaderGenerator.new(loader_path, buffer).call(&amp;block)
      buffer.rewind
    end.read == current
  end

  private

  def current
    File.read(loader_path)
  end

  attr_reader :loader_path

  def initialize(loader_path)
    @loader_path = loader_path
  end
end

require 'bundler/setup'

loader_path = File.join(__dir__, '..', 'lib', 'basic_loader.rb')

klass = ARGV[0] == 'generate' ? LoaderGenerator : LoaderValidator
action = klass.new(loader_path)
result = action.call do
  require 'zeitgeist'
  Zeitgeist::AUTOLOADERS
end
exit 1 unless result</code></code></pre><p>Now make the file executable and run it:</p><pre><code><code>$ chmod +x bin/loader
$ bin/loader generate</code></code></pre><p>It will:</p><ol><li><p>Initialize Zeitwerk and the main module of our gem.</p></li><li><p>Configure Zeitwerk to add <em>require</em> line to the basic loader each time the Ruby file is loaded.</p></li><li><p>Tell Zeitwerk to eagerly load all the code. As a result, <em>lib/basic_loader.rb</em> becomes populated.</p></li></ol><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nx-p!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nx-p!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.jpeg 424w, https://substackcdn.com/image/fetch/$s_!nx-p!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.jpeg 848w, https://substackcdn.com/image/fetch/$s_!nx-p!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!nx-p!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nx-p!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.jpeg" width="1456" height="661" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:661,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1294531,&quot;alt&quot;:&quot;A classic fresco depicting almighty Zeitwerk (God) creating a basic loader (Adam) which says: \&quot;Thx for creating me Z. Got runtime. See you in dev.\&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A classic fresco depicting almighty Zeitwerk (God) creating a basic loader (Adam) which says: &quot;Thx for creating me Z. Got runtime. See you in dev.&quot;" title="A classic fresco depicting almighty Zeitwerk (God) creating a basic loader (Adam) which says: &quot;Thx for creating me Z. Got runtime. See you in dev.&quot;" srcset="https://substackcdn.com/image/fetch/$s_!nx-p!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.jpeg 424w, https://substackcdn.com/image/fetch/$s_!nx-p!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.jpeg 848w, https://substackcdn.com/image/fetch/$s_!nx-p!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!nx-p!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Inspired by <em>The Creation of Adam</em> (~1511) by Michelangelo, fresco painting, 230.1 cm &#215; 480.1 cm, Sistine Chapel</figcaption></figure></div><p>The file <em>lib/basic_loader.rb</em> should look like this:</p><pre><code><code># frozen_string_literal: true

# File generated automatically, do not edit

require 'zeitgeist/programmer'</code></code></pre><h2>Ensure the built gem contains current files</h2><p>If you read the <em>bin/loader</em> code above carefully, you probably noticed <em>LoaderValidator</em> class. It gets activated if <em>generate</em> argument was not specified. It generates a basic loader in memory and compares it with the current. If they don&#8217;t match, the script exits with an error.</p><p>We will use it to protect us from forgetting to regenerate the basic loader before building the gem.</p><p>Let&#8217;s add those lines to the end of <em>Rakefile</em>:</p><pre><code><code>Rake::Task.define_task :validate_loader do
  abort "Basic loader is stale, run `bin/loader generate` to fix" unless system("bin/loader validate")
end

Rake::Task[:build].enhance [:validate_loader]</code></code></pre><p>We create a new task called <em>validate_loader</em> which raises an exception if the basic loader is stale. Afterward, we add it as a dependency to the <em>build</em> task, so it gets called before.</p><p>From now on running <em>rake build</em> with stale basic loader will fail with the message:</p><pre><code><code>Basic loader is stale, run `bin/loader generate` to fix</code></code></pre><h2>Tests: which loader to use?</h2><p>It makes more sense to use the basic loader instead of Zeitwerk for tests. What if we generate the basic loader incorrectly due to some error? If tests use Zeitwerk, the basic loader is bypassed and we won&#8217;t see any problems.</p><p>As we use RSpec for our gem, let&#8217;s add this line to the beginning of <em>spec/spec_helper.rb</em>:</p><pre><code><code>raise "Failed to regenerate basic loader" unless system "bin/loader generate"</code></code></pre><p>This way before each test, the basic loader will be regenerated automatically.</p><p>If you prefer to decide when the basic loader file changes, you can use this instead:</p><pre><code><code>raise "Basic loader is stale, run `bin/loader generate` to fix" unless system "bin/loader validate"</code></code></pre><p>You will be notified when the basic loader needs to be updated. This approach allows you to have full control over updating this file.</p><h2>Run the tests</h2><p>We should have a working infrastructure, let&#8217;s run the tests:</p><pre><code><code>$ rake
... 
Zeitgeist
  has a version number
  does something useful (FAILED - 1)
...</code></code></pre><p>The second test failed. It&#8217;s an example test that is meant to fail. Let&#8217;s change it to something related to our logic by replacing <em>spec/zeitgeist_spec.rb</em> with:</p><pre><code><code>RSpec.describe Zeitgeist do
  it "has a version number" do
    expect(Zeitgeist::VERSION).not_to be nil
  end

  it "checks if programmer is happy" do
    expect(Zeitgeist::Programmer.new.happy?).to eq("yes")
  end
end</code></code></pre><p>Run the tests again:</p><pre><code><code>$ rake
...
Zeitgeist
  has a version number
  checks if programmer is happy
...</code></code></pre><p>All green! Let&#8217;s commit our changes, build the gem and install it:</p><pre><code><code>$ git add lib spec bin/loader Rakefile
$ git commit -m 'Make sure basic loader is autogenerated'
$ rake build
$ gem install --user pkg/zeitgeist-0.1.0.gem</code></code></pre><h2>Final run</h2><p>We can check if the gem works after installation:</p><pre><code><code>$ irb
&gt; require 'zeitgeist'
&gt; Zeitgeist::Programmer.new.happy?
=&gt; "yes"
&gt; defined? Zeitwerk
=&gt; nil</code></code></pre><p>It works, and it still doesn&#8217;t require Zeitwerk.</p><p>Now let&#8217;s test if Zeitwerk works in development. First, let&#8217;s create a random constant:</p><pre><code><code>$ echo "Zeitgeist::Pi = 3.1337" &gt; lib/zeitgeist/pi.rb</code></code></pre><p>This file is not required anywhere. Let&#8217;s check if it autoloads:</p><pre><code><code>$ bin/console
&gt; Zeitgeist::Pi
=&gt; 3.1337</code></code></pre><p>It works. From now on, you can easily add new files to your code base and do not worry about <em>require</em> statements. Congratulations :)</p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">If that was interesting, subscribe! You will receive my upcoming posts in your mailbox. 100% spam free, unsubscribe anytime.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>Cleanup</h2><p>To restore your environment to its previous state, you will need to uninstall the gem:</p><pre><code><code>$ gem uninstall zeitgeist</code></code></pre><p>You can also remove the directory containing the repository you were working on. Apart from the code, all the gems you installed including Zeitwerk are located there.</p><h2>Limitations</h2><p>Compared to using Zeitwerk, this approach has some limitations. I believe in most cases, minor code adjustments would do. As with every refactoring, good test coverage will help. However, if your project is large and/or you use advanced code-loading features, implementation of this approach may be challenging.</p><p>Those limitations were identified by the author of Zeitwerk, Xavier Noria. He gave me <a href="https://github.com/fxn/zeitwerk/issues/271">awesome feedback</a> which resulted in a major update to the article (the old version could be found <a href="https://web.archive.org/web/20230821123926/https://blog.pawelpokrywka.com/p/gem-with-zeitwerk-as-development-only-dependency">here</a>). Thank you, Xavier!</p><p>Here are the limitations I&#8217;m currently aware of:</p><h4>Conditional code loading</h4><p>An example would be to use one module on Linux, and another on macOS:</p><pre><code><code>module Foo
  include Mac if mac?
  include Linux if linux?
end</code></code></pre><p>If OS-specific modules are defined in separate files, the basic loader will be generated differently depending on the developer&#8217;s OS.</p><p>Effectively gem developed on Linux will crash on macOS and vice-versa.</p><h4>Delayed code loading</h4><p>The developer may choose to delay the loading of some code parts. As the method presented in this post depends on eager loading, it won't include those parts. This will lead to crashes in runtime.</p><h4>Circular references</h4><p>Let&#8217;s assume we have <em>foo.rb</em>:</p><pre><code><code>module Foo
  include Bar
end</code></code></pre><p>And <em>foo/bar.rb</em>:</p><pre><code><code>module Foo::Bar
end</code></code></pre><p>Zeitwerk can handle this, while the generated basic loader will crash.</p><h4>Implicit namespace definitions</h4><p>Zeitwerk allows you to define <em>Zeitgeist::Foo::Bar</em> class without defining <em>Zeitgeist::Foo</em> module first. If you want to use the approach presented in this post, you need to define it explicitly.</p><h2>Final notes</h2><p>You learned how to use Zeitwerk in gem development while not adding it as a runtime dependency. <a href="https://github.com/pepawel/zeitgeist">Here you will find the repository</a> with the gem we just built together.</p><p>I encourage you to apply this knowledge to your own project. The code I published here uses a permissive MIT license, so you can use it even in your commercial work.</p><p>Also, there are <a href="https://rubygems.org/gems/zeitwerk/reverse_dependencies">nearly 450 gems depending on Zeitwerk</a>. I think many of them would accept pull requests minimizing runtime dependencies. If you feel brave, go contribute to your favorite gem to make the Open Source world a better place!</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PnZR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PnZR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.png 424w, https://substackcdn.com/image/fetch/$s_!PnZR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.png 848w, https://substackcdn.com/image/fetch/$s_!PnZR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.png 1272w, https://substackcdn.com/image/fetch/$s_!PnZR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PnZR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.png" width="385" height="489" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:489,&quot;width&quot;:385,&quot;resizeWidth&quot;:385,&quot;bytes&quot;:23987,&quot;alt&quot;:&quot;A complicated stack of blocks symbolizing all the modern infrastructure. One small block at the bottom symbolizes a project some random person in Nebraska has been thanklessly maintaining since 2003&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A complicated stack of blocks symbolizing all the modern infrastructure. One small block at the bottom symbolizes a project some random person in Nebraska has been thanklessly maintaining since 2003" title="A complicated stack of blocks symbolizing all the modern infrastructure. One small block at the bottom symbolizes a project some random person in Nebraska has been thanklessly maintaining since 2003" srcset="https://substackcdn.com/image/fetch/$s_!PnZR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.png 424w, https://substackcdn.com/image/fetch/$s_!PnZR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.png 848w, https://substackcdn.com/image/fetch/$s_!PnZR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.png 1272w, https://substackcdn.com/image/fetch/$s_!PnZR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Please don&#8217;t get me wrong. Zeitwerk is a rock-solid, actively developed gem. I highly respect the developers behind it and I am grateful to them. But I still believe limiting your dependencies is beneficial. Source: <a href="https://xkcd.com/2347/">xkcd</a></figcaption></figure></div><p>The code used in this tutorial was extracted from the <a href="https://www.pawelpokrywka.com/p/cryptreboot">cryptreboot</a> gem. This little project is very close to my heart, so forgive me I mentioned it for a second time in this post. If you use a Linux system with an encrypted disk, give cryptreboot a try :)</p><p>And thank you for reading this post :) You are the best!</p><p>If you have any feedback, please leave it in a comment below.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>At <a href="https://www.youtube.com/watch?v=oEkJvvGEtB4">this</a> Google Tech Talk in 2008, Matz said, &#8220;I hope to see Ruby help every programmer in the world to be productive, and to enjoy programming, and to be happy. That is the primary purpose of Ruby language.&#8221;</p><p></p></div></div>]]></content:encoded></item></channel></rss>