<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Osmium</title>
    <description>The latest articles on DEV Community by Osmium (@osag).</description>
    <link>https://dev.to/osag</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3876023%2F3b75b958-9068-407d-84bb-8e881c71f025.png</url>
      <title>DEV Community: Osmium</title>
      <link>https://dev.to/osag</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/osag"/>
    <language>en</language>
    <item>
      <title>Turn your OpenWrt router into a quorum device for Proxmox VE cluster</title>
      <dc:creator>Osmium</dc:creator>
      <pubDate>Fri, 01 May 2026 14:26:19 +0000</pubDate>
      <link>https://dev.to/osag/turn-your-openwrt-router-into-a-quorum-device-for-proxmox-ve-cluster-561m</link>
      <guid>https://dev.to/osag/turn-your-openwrt-router-into-a-quorum-device-for-proxmox-ve-cluster-561m</guid>
      <description>&lt;h2&gt;
  
  
  Why Bother
&lt;/h2&gt;

&lt;p&gt;I have a two-node Proxmox VE cluster, and two-node clusters have an inherent distributed systems problem: if either machine goes down, the surviving node can't tell whether it should keep running or not. Running a quorum usually requires a majority vote, which normally means at least three voters. With only two machines, one vote each, lose one and you're down to one vote. No quorum and your cluster stops.&lt;/p&gt;

&lt;p&gt;The solution is a quorum device. Proxmox supports &lt;code&gt;corosync-qnetd&lt;/code&gt;, a lightweight daemon from the corosync project that acts as a third-party tiebreaker. A qnetd doesn't participate in storage or compute, it just casts the deciding vote during a split-brain scenario. By bringing the total vote count to three, the cluster maintains a majority requirement and consequently, if any node fails, the surviving node and the router’s vote can collectively sustain quorum, making the cluster keeps running.&lt;/p&gt;

&lt;p&gt;The ideal host for something like this is a device in a home network that basically never gets turned off, like a router. &lt;/p&gt;

&lt;p&gt;I had a Xiaomi Mini router running OpenWrt, MT7620 chip, 128MB DDR2 memory, 16MB SPI Flash, mipsel_24kc architecture. It runs 24/7, draws a few watts, perfect for the job. The problem: there's no corosync-qnetd package for mipsel in the official OpenWrt feeds. :(&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffblrpdc0er8lbnbkkr41.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffblrpdc0er8lbnbkkr41.jpg" alt="An online image of Xiaomi Mini Router" width="474" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding a Starting Point
&lt;/h2&gt;

&lt;p&gt;After some searching, I found someone on GitHub who had compiled corosync-qnetd for aarch64 (ARM 64-bit) OpenWrt, targeting GL.iNet routers and using the old opkg/ipk package format. A good starting point.&lt;/p&gt;

&lt;p&gt;I had essentially zero experience with C, Makefiles, or cross-compilation packaging. I knew C programs need a Makefile to build, but that was about it. So my approach was simply throw it at Claude, hit an error, ask Claude about the error, hit the next error, ask about that one like a no-brainer. &lt;strong&gt;Six hours of tweaking compile parameters over and over, eventually it compiled successfully (phew). I installed it on the router, and it worked!&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dependency Investigation
&lt;/h2&gt;

&lt;p&gt;But I needed to understand &lt;em&gt;why&lt;/em&gt; it works. Time to do a proper investigation. &lt;/p&gt;

&lt;p&gt;I started digging into the Makefile syntax, the build parameters, and the dependency structure. But mastering every parameter would be impossible because many of them are deeply encapsulated, documented in their own specific docs with their own quirks. &lt;/p&gt;

&lt;p&gt;My approach is simply &lt;strong&gt;upstream-first&lt;/strong&gt;, and I wanted to use official packages whenever possible to maintain generality. The original project compiled custom versions of &lt;code&gt;NSS&lt;/code&gt;, &lt;code&gt;NSPR&lt;/code&gt;, &lt;code&gt;libknet&lt;/code&gt;, and &lt;code&gt;libqb&lt;/code&gt;, plus a homebrewed &lt;code&gt;corosync-nss-tools&lt;/code&gt; and that's a lot of custom packages to maintain.&lt;/p&gt;

&lt;p&gt;To verify what was actually necessary, I used a trick from Claude during our earlier sessions: running &lt;code&gt;ldd&lt;/code&gt; against a binary shows you exactly which shared libraries it actually links to at runtime. So I ran it against the &lt;code&gt;corosync-qnetd&lt;/code&gt; binary on the router:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ldd /usr/sbin/corosync-qnetd
  /lib/ld-musl-mipsel-sf.so.1
  libnss3.so &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; /usr/lib/libnss3.so
  libssl3.so &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; /usr/lib/libssl3.so
  libnspr4.so &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; /usr/lib/libnspr4.so
  libgcc_s.so.1 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; /lib/libgcc_s.so.1
  libc.so &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; /lib/ld-musl-mipsel-sf.so.1
  libnssutil3.so &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; /usr/lib/libnssutil3.so
  libplc4.so &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; /usr/lib/libplc4.so
  libplds4.so &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; /usr/lib/libplds4.so
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's pretty much it, NSS and NSPR. Nothing else.&lt;/p&gt;

&lt;p&gt;Which means that of the four dependencies bundled in the original project, two of them, &lt;code&gt;libknet&lt;/code&gt; and &lt;code&gt;libqb&lt;/code&gt;, were completely unnecessary for qnetd. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;libknet&lt;/code&gt; handles inter-node network communication for the corosync cluster daemon itself; &lt;code&gt;libqb&lt;/code&gt; provides logging and IPC for the corosync main process. &lt;/p&gt;

&lt;p&gt;But the &lt;code&gt;qnetd&lt;/code&gt; server is an independent program, it doesn't use either of them. Not to mention the author's project README said it was for qnetd, yet it compiled and bundled libraries that only the cluster daemon needs. My best guess is that they looked at corosync's upstream README, which lists all dependencies for the full corosync project (including the qdevice client), and just compiled everything at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Portability Principle
&lt;/h3&gt;

&lt;p&gt;My goal became: &lt;strong&gt;compile only corosync-qnetd itself, and let the package manager handle everything else through official repositories.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The final dependency chain is clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;corosync-qnetd (custom-compiled, the only custom package)
├── libnss    ← official OpenWrt package
├── nspr   ← official OpenWrt package (pulled in by libnss)
├── nss-utils ← official OpenWrt package (includes `certutil`, `pk12util` used for certificate management)
└── openssl-util ← official OpenWrt package (needed by the `certutil` signing script)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Technical Challenges
&lt;/h2&gt;

&lt;p&gt;With the "what" and "why" covered, here are the significant technical problems I encountered and how they were solved, in roughly the order I hit them.&lt;/p&gt;

&lt;h3&gt;
  
  
  NSS Cross-Compilation: The Deepest Pit
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;corosync-qnetd&lt;/code&gt; depends on Mozilla NSS (Network Security Services) for TLS. NSS doesn't use autoconf, it has its own build system called &lt;code&gt;coreconf&lt;/code&gt;, driven by environment variables to determine the target platform. The key variables are defined in &lt;code&gt;coreconf/arch.mk&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nv"&gt;OS_ARCH&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;subst /,_,&lt;span class="p"&gt;$(&lt;/span&gt;shell &lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c"&gt;# e.g. "Linux"&lt;/span&gt;
&lt;span class="nv"&gt;OS_TEST&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;shell &lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c"&gt;# e.g. "x86_64", "mipsel"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;OS_ARCH&lt;/code&gt; equals &lt;code&gt;Linux&lt;/code&gt;, the build system loads &lt;code&gt;coreconf/Linux.mk&lt;/code&gt; for platform-specific flags. The problem is: in a cross-compilation environment, &lt;code&gt;uname -s&lt;/code&gt; and &lt;code&gt;uname -m&lt;/code&gt; return the host machine's values, not the target's. If you don't override these variables, NSS would happily detects my &lt;code&gt;x86_64&lt;/code&gt; build server, uses the host's &lt;code&gt;cc&lt;/code&gt;, and produces x86 object files. The link stage then explodes because you're trying to link x86 objects against mipsel libraries.&lt;/p&gt;

&lt;p&gt;The original aarch64 project had set &lt;code&gt;OS_ARCH=aarch64&lt;/code&gt; with &lt;code&gt;USE_64=1&lt;/code&gt;, which was non-standard, the &lt;a href="https://github.com/openwrt/packages/blob/master/libs/nss/Makefile" rel="noopener noreferrer"&gt;OpenWrt official NSS Makefile&lt;/a&gt; uses &lt;code&gt;OS_ARCH=Linux&lt;/code&gt; and &lt;code&gt;OS_TEST=$(ARCH)&lt;/code&gt; instead. The aarch64 build likely worked despite the non-standard &lt;code&gt;OS_ARCH&lt;/code&gt; value due to other variables compensating, but that approach couldn't be carried over to mipsel.&lt;/p&gt;

&lt;p&gt;Diagnosing this took a while. The build log showed a wall of link errors that initially looked like missing libraries. The real clue was checking the object files with &lt;code&gt;file&lt;/code&gt; command, they were all x86 binaries, not mipsel.&lt;/p&gt;

&lt;p&gt;The fix was to follow the same pattern as OpenWrt's official NSS package, explicitly tell NSS everything it needs to know about the target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nv"&gt;OS_ARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Linux        &lt;span class="c"&gt;# load coreconf/Linux.mk&lt;/span&gt;
&lt;span class="nv"&gt;OS_TARGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Linux      &lt;span class="c"&gt;# what NSS actually checks internally&lt;/span&gt;
&lt;span class="nv"&gt;OS_TEST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mipsel       &lt;span class="c"&gt;# equivalent to uname -m on the target&lt;/span&gt;
&lt;span class="nv"&gt;CPU_ARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mipsel      &lt;span class="c"&gt;# target CPU architecture&lt;/span&gt;
&lt;span class="c"&gt;# USE_64 not set    # mipsel_24kc is 32-bit
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Through all of this I also learned some C compilation basics, every dependency's headers and libraries need to be present in the build environment before anything will compile, and how OpenWrt's feed system works to pull in those dependencies automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  NSPR Header File Mess
&lt;/h3&gt;

&lt;p&gt;Corosync depends on nss, and nss depends on NSPR (Netscape Portable Runtime), NSS's sub-makefiles hardcode the search paths for NSPR headers and libraries, paths like &lt;code&gt;dist/&amp;lt;OBJDIR&amp;gt;/include&lt;/code&gt; and &lt;code&gt;dist/public/nspr&lt;/code&gt;. Setting the &lt;code&gt;NSPR_INCLUDE_DIR&lt;/code&gt; environment variable isn't enough because different NSS build modules look for NSPR in different places using different methods.&lt;/p&gt;

&lt;p&gt;The fix was to manually stage NSPR's headers and libraries into the directory structure NSS expects before compilation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="err"&gt;mkdir&lt;/span&gt; &lt;span class="err"&gt;-p&lt;/span&gt; &lt;span class="err"&gt;$(PKG_BUILD_DIR)/dist/target.OBJ/include&lt;/span&gt;
&lt;span class="err"&gt;cp&lt;/span&gt; &lt;span class="err"&gt;-fpRL&lt;/span&gt; &lt;span class="err"&gt;$(STAGING_DIR)/usr/include/nspr/.&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
    &lt;span class="err"&gt;$(PKG_BUILD_DIR)/dist/target.OBJ/include/&lt;/span&gt;
&lt;span class="err"&gt;mkdir&lt;/span&gt; &lt;span class="err"&gt;-p&lt;/span&gt; &lt;span class="err"&gt;$(PKG_BUILD_DIR)/dist/public/nspr&lt;/span&gt;
&lt;span class="err"&gt;cp&lt;/span&gt; &lt;span class="err"&gt;-fpRL&lt;/span&gt; &lt;span class="err"&gt;$(STAGING_DIR)/usr/include/nspr/.&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
    &lt;span class="err"&gt;$(PKG_BUILD_DIR)/dist/public/nspr/&lt;/span&gt;
&lt;span class="err"&gt;mkdir&lt;/span&gt; &lt;span class="err"&gt;-p&lt;/span&gt; &lt;span class="err"&gt;$(PKG_BUILD_DIR)/dist/target.OBJ/lib&lt;/span&gt;
&lt;span class="err"&gt;for&lt;/span&gt; &lt;span class="err"&gt;lib&lt;/span&gt; &lt;span class="err"&gt;in&lt;/span&gt; &lt;span class="err"&gt;libnspr4.so&lt;/span&gt; &lt;span class="err"&gt;libplc4.so&lt;/span&gt; &lt;span class="err"&gt;libplds4.so;&lt;/span&gt; &lt;span class="err"&gt;do&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
    &lt;span class="err"&gt;cp&lt;/span&gt; &lt;span class="err"&gt;-fpL&lt;/span&gt; &lt;span class="err"&gt;$(STAGING_DIR)/usr/lib/$$lib&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
        &lt;span class="err"&gt;$(PKG_BUILD_DIR)/dist/target.OBJ/lib/&lt;/span&gt; &lt;span class="err"&gt;2&amp;gt;/dev/null&lt;/span&gt; &lt;span class="err"&gt;||&lt;/span&gt; &lt;span class="err"&gt;true;&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
&lt;span class="err"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A host-native version of &lt;code&gt;nsinstall&lt;/code&gt; (a build tool NSS needs to run on the host during compilation) also had to be compiled first, otherwise it would try to run a mipsel binary on the host machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  The ar Parameter
&lt;/h3&gt;

&lt;p&gt;All &lt;code&gt;.c&lt;/code&gt; files compiled correctly with the cross-compiler, but linking produced an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mipsel-openwrt-linux-musl-ar cr target.OBJ/arena.o target.OBJ/error.o ...
mipsel-openwrt-linux-musl-ar: target.OBJ/arena.o: file format not recognized
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ar&lt;/code&gt; was treating the first &lt;code&gt;.o&lt;/code&gt; file as an existing archive. The cause: NSS's makefile adds &lt;code&gt;cr&lt;/code&gt; flags when invoking &lt;code&gt;ar&lt;/code&gt;, but I had also set &lt;code&gt;AR="$(TARGET_CROSS)ar cr"&lt;/code&gt; in the OpenWrt Makefile. Two &lt;code&gt;cr&lt;/code&gt;s stacked together, mangling the command-line arguments entirely. The fix was changing &lt;code&gt;AR&lt;/code&gt; from &lt;code&gt;$(TARGET_CROSS)ar cr&lt;/code&gt; to just &lt;code&gt;$(TARGET_CROSS)ar&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trimming NSS: Cut to the Bone
&lt;/h3&gt;

&lt;p&gt;A full NSS build is painfully slow and drags in &lt;code&gt;sqlite3&lt;/code&gt; and a bunch of other dependencies. corosync-qnetd only uses a small subset of NSS functionality. Only the six &lt;code&gt;.so&lt;/code&gt; files qnetd actually needs were kept: &lt;code&gt;libnss3&lt;/code&gt;, &lt;code&gt;libssl3&lt;/code&gt;, &lt;code&gt;libsmime3&lt;/code&gt;, &lt;code&gt;libnssutil3&lt;/code&gt;, &lt;code&gt;libsoftokn3&lt;/code&gt;, and &lt;code&gt;libfreebl3&lt;/code&gt;. The original project already had the idea here, it used &lt;code&gt;NSS_DISABLE_DBM=1&lt;/code&gt; to eliminate the sqlite3 dependency and &lt;code&gt;NSS_DISABLE_LIBPKIX=1&lt;/code&gt; to skip PKIX certificate path validation. I kept both of these flags. But the package relationship required careful handling, so I created a separate package called &lt;code&gt;nss-qnetd&lt;/code&gt; instead of reusing the original's NSS build, for two reasons.&lt;/p&gt;

&lt;p&gt;First, naming. The package needed to coexist with the official &lt;code&gt;libnss&lt;/code&gt; in OpenWrt's package system without conflicting. nss-qnetd declares &lt;code&gt;PROVIDES:=libnss&lt;/code&gt; so OpenWrt's package manager can resolve the &lt;code&gt;libnss3.so&lt;/code&gt; dependency at compile time. But at runtime on the router, corosync-qnetd's runtime dependency points to the official &lt;code&gt;libnss&lt;/code&gt; from the feeds — compile with the stripped version for headers and libraries, run with the package from upstream.&lt;/p&gt;

&lt;p&gt;Second, improved robustness. The original project used patch files to modify NSS source files like &lt;code&gt;nss/cmd/manifest.mn&lt;/code&gt;. Patches depend on exact line numbers, when I upgraded to NSS 3.112, the line counts didn't match and the patch failed with &lt;code&gt;malformed patch at line 3&lt;/code&gt;, so by replacing all patches with direct shell commands in &lt;code&gt;Build/Compile&lt;/code&gt; gives it a more robust way of injecting params.&lt;/p&gt;

&lt;h3&gt;
  
  
  The custom &lt;code&gt;corosync-nss-tools&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Then there was &lt;code&gt;corosync-nss-tools&lt;/code&gt;, a package the original author created that doesn't exist upstream. It bundled &lt;code&gt;certutil&lt;/code&gt; and &lt;code&gt;pk12util&lt;/code&gt; (NSS command-line tools for managing certificate databases) together with custom shell wrapper scripts that reimplemented the certificate initialisation logic. But OpenWrt's official feeds already have an &lt;code&gt;nss-utils&lt;/code&gt; package that provides certutil and pk12util. And the upstream &lt;code&gt;corosync-qdevice&lt;/code&gt; project already ships its own &lt;code&gt;corosync-qnetd-certutil&lt;/code&gt; script. There was no need to repackage binaries that are already available or to rewrite scripts that upstream already maintains. Removing &lt;code&gt;corosync-nss-tools&lt;/code&gt; entirely and depending on the official &lt;code&gt;nss-utils&lt;/code&gt; was the straightforward improvement.&lt;/p&gt;

&lt;h3&gt;
  
  
  The musl dladdr Trap
&lt;/h3&gt;

&lt;p&gt;This fix was inherited from the original project and is worth documenting. NSS's &lt;code&gt;libsoftokn3.so&lt;/code&gt; calls &lt;code&gt;dladdr()&lt;/code&gt; at runtime to find its own file path, then uses that path to locate and load &lt;code&gt;libfreebl3.so&lt;/code&gt;. This works fine on glibc, but musl's &lt;code&gt;dladdr()&lt;/code&gt; implementation behaves differently — in certain cases it can't correctly resolve the path of an already-loaded library, causing softokn to fail to find freebl. Mozilla bug tracker &lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=511312" rel="noopener noreferrer"&gt;Bug 511312&lt;/a&gt; confirmed this.&lt;/p&gt;

&lt;p&gt;The original author's fix was to use &lt;code&gt;patchelf&lt;/code&gt; after compilation to add an explicit dependency on &lt;code&gt;libfreebl3.so&lt;/code&gt; to &lt;code&gt;libsoftokn3.so&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="err"&gt;find&lt;/span&gt; &lt;span class="err"&gt;$(PKG_BUILD_DIR)/dist&lt;/span&gt; &lt;span class="err"&gt;-name&lt;/span&gt; &lt;span class="err"&gt;libsoftokn3.so&lt;/span&gt; &lt;span class="err"&gt;-exec&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
    &lt;span class="err"&gt;$(STAGING_DIR_HOST)/bin/patchelf&lt;/span&gt; &lt;span class="err"&gt;--add-needed&lt;/span&gt; &lt;span class="err"&gt;libfreebl3.so&lt;/span&gt; &lt;span class="err"&gt;{}&lt;/span&gt; &lt;span class="err"&gt;\;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the dynamic linker automatically load freebl when loading softokn, bypassing the &lt;code&gt;dladdr()&lt;/code&gt; issue entirely. Notably, the &lt;a href="https://github.com/openwrt/packages/blob/master/libs/nss/Makefile" rel="noopener noreferrer"&gt;OpenWrt official NSS Makefile&lt;/a&gt; uses a related approach — it sets &lt;code&gt;FREEBL_NO_DEPEND=1&lt;/code&gt; to handle the same &lt;code&gt;softokn-freebl&lt;/code&gt; loading relationship.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenWrt 25.12: Big Changes to Package Management
&lt;/h3&gt;

&lt;p&gt;With cross-compilation sorted, I ran into a series of changes in the latest OpenWrt 25.12.&lt;/p&gt;

&lt;p&gt;The biggest change was the package manager switch from opkg to apk (Alpine Package Keeper). Output format changed from &lt;code&gt;.ipk&lt;/code&gt; to &lt;code&gt;.apk&lt;/code&gt;. Fortunately, the OpenWrt SDK 25.12 natively outputs apk format, I just needed to update all &lt;code&gt;.ipk&lt;/code&gt; references to &lt;code&gt;.apk&lt;/code&gt; in the build scripts.&lt;/p&gt;

&lt;p&gt;But after installing the corosync-qnetd apk, more insidious problems were at runtime. Running the certificate initialization script &lt;code&gt;corosync-qnetd-certutil -i&lt;/code&gt; produced a barrage of errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;stat: applet not found
chown: unrecognized option '--reference'
sha1sum: command not found
ps: unrecognized option: e
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OpenWrt's userspace tools are mostly BusyBox, stripped-down versions with limited options. BusyBox's &lt;code&gt;chown&lt;/code&gt; doesn't support &lt;code&gt;--reference&lt;/code&gt;, the &lt;code&gt;stat&lt;/code&gt; applet wasn't compiled into this firmware's BusyBox, &lt;code&gt;ps&lt;/code&gt; doesn't support the &lt;code&gt;-e&lt;/code&gt; flag. The certutil script was written for a full GNU/Linux environment and fell apart in BusyBox land.&lt;/p&gt;

&lt;p&gt;Full GNU versions of these tools were needed. But here was another trap: OpenWrt 25.12 splits coreutils into dozens of individual sub-packages (&lt;code&gt;coreutils-chown&lt;/code&gt;, &lt;code&gt;coreutils-stat&lt;/code&gt;, &lt;code&gt;coreutils-sha1sum&lt;/code&gt;, ...). The main &lt;code&gt;coreutils&lt;/code&gt; package is an empty shell — installing it installs nothing. &lt;code&gt;apk info -L coreutils&lt;/code&gt; outputs exactly one line: &lt;code&gt;lib/apk/packages/coreutils.list&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The final list of runtime dependencies that needed to be installed individually: &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;coreutils-chown&lt;/code&gt;, &lt;code&gt;coreutils-stat&lt;/code&gt;, &lt;code&gt;coreutils-sha1sum&lt;/code&gt;, &lt;code&gt;procps-ng&lt;/code&gt; (for a full &lt;code&gt;ps&lt;/code&gt;), &lt;code&gt;openssh-sftp-server&lt;/code&gt; (PVE's scp defaults to SFTP mode — without sftp-server on the router, certificate copying fails), and &lt;code&gt;openssl-util&lt;/code&gt; (the certutil signing script needs the openssl command-line tool).&lt;/p&gt;

&lt;p&gt;All of these were added to the Makefile's &lt;code&gt;DEPENDS&lt;/code&gt;, so a &lt;code&gt;apk add corosync-qnetd&lt;/code&gt; pulls in everything automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Final Result
&lt;/h2&gt;

&lt;p&gt;After all the pitfalls were navigated, the end product is one clean OpenWrt package. Installation only takes a single command and all dependencies are pulled automatically. Initialise the certificate database (key generation takes about five to ten minutes on mipsel hardware), start the service, then run &lt;code&gt;pvecm qdevice setup &amp;lt;router IP&amp;gt;&lt;/code&gt; on any PVE node. Certificates exchange automatically, and the cluster recognises the Qdevice.&lt;/p&gt;

&lt;p&gt;The core changes in the final Makefile: corrected NSS architecture parameters from aarch64 to the proper mipsel 32-bit combination; trimmed NSS to compile only the libraries qnetd needs; added all the runtime dependencies and removed unnecessary libknet and libqb; migrated from ipk to apk.&lt;/p&gt;

&lt;p&gt;Porting corosync-qnetd to mipsel means thousands of MIPS-based routers running OpenWrt can now serve as quorum arbitrators for Proxmox clusters. &lt;/p&gt;

&lt;p&gt;A full day's work, morning to night. Worth it.&lt;/p&gt;

&lt;p&gt;GitHub Releases: &lt;a href="https://github.com/osmiumsilver/corosync-qnetd-openwrt-mipsel/releases" rel="noopener noreferrer"&gt;https://github.com/osmiumsilver/corosync-qnetd-openwrt-mipsel/releases&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  More Links
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;NSS Build System&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://hg.mozilla.org/mozilla-central/file/tip/security/nss/coreconf/arch.mk" rel="noopener noreferrer"&gt;NSS &lt;code&gt;coreconf/arch.mk&lt;/code&gt;&lt;/a&gt; — Where &lt;code&gt;OS_ARCH&lt;/code&gt;, &lt;code&gt;OS_TEST&lt;/code&gt;, and &lt;code&gt;OS_TARGET&lt;/code&gt; are resolved. Shows that &lt;code&gt;Linux.mk&lt;/code&gt; is included whenever &lt;code&gt;OS_ARCH=Linux&lt;/code&gt;, regardless of CPU architecture.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/servo/nss/blob/master/coreconf/Linux.mk" rel="noopener noreferrer"&gt;NSS &lt;code&gt;coreconf/Linux.mk&lt;/code&gt;&lt;/a&gt; — Platform-specific flags for Linux targets, including the &lt;code&gt;AR&lt;/code&gt; definition that causes the double-&lt;code&gt;cr&lt;/code&gt; issue.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://fossies.org/linux/nss/nss/coreconf/detect_host_arch.py" rel="noopener noreferrer"&gt;NSS &lt;code&gt;coreconf/detect_host_arch.py&lt;/code&gt;&lt;/a&gt; — Host architecture detection script; recognizes mips/mips64 but only for host detection, not cross-compilation.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/openwrt/packages/blob/master/libs/nss/Makefile" rel="noopener noreferrer"&gt;OpenWrt official NSS Makefile&lt;/a&gt; — Reference implementation for cross-compiling NSS on OpenWrt. Uses &lt;code&gt;OS_ARCH=Linux&lt;/code&gt;, &lt;code&gt;OS_TEST=$(ARCH)&lt;/code&gt;, and &lt;code&gt;FREEBL_NO_DEPEND=1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;corosync-qdevice / corosync-qnetd&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/osmiumsilver/corosync-qnetd-openwrt-mipsel" rel="noopener noreferrer"&gt;osmiumsilver/corosync-qnetd-openwrt-mipsel&lt;/a&gt; —  My mipsel port to OpenWrt 25.12+&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/corosync/corosync-qdevice" rel="noopener noreferrer"&gt;corosync/corosync-qdevice&lt;/a&gt; — Upstream source.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/jrparks/corosync-qnetd-openwrt" rel="noopener noreferrer"&gt;jrparks/corosync-qnetd-openwrt&lt;/a&gt; — The original aarch64 OpenWrt project this work was forked from.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Community Discussions&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://forum.openwrt.org/t/porting-corosync-qdevice-to-openwrt/193940" rel="noopener noreferrer"&gt;Porting corosync-qdevice to OpenWrt&lt;/a&gt; — OpenWrt forum thread on the challenges of building corosync-qdevice for OpenWrt.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://forum.proxmox.com/threads/corosync-qnetd-on-openwrt.181963/" rel="noopener noreferrer"&gt;Corosync QNetd on OpenWrt — Proxmox Forum&lt;/a&gt; — jrparks' original announcement of the aarch64 build.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Entware/Entware/issues/994" rel="noopener noreferrer"&gt;Package Request: corosync-qdevice — Entware&lt;/a&gt; — Earlier request for corosync-qdevice on mipsel.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>proxmox</category>
      <category>corosync</category>
      <category>homelab</category>
      <category>openwrt</category>
    </item>
    <item>
      <title>How a missing @ in a filename broke my Netlify build</title>
      <dc:creator>Osmium</dc:creator>
      <pubDate>Mon, 27 Apr 2026 12:18:09 +0000</pubDate>
      <link>https://dev.to/osag/how-a-missing-in-a-filename-broke-my-netlify-build-10k</link>
      <guid>https://dev.to/osag/how-a-missing-in-a-filename-broke-my-netlify-build-10k</guid>
      <description>&lt;h2&gt;
  
  
  It was fine three months ago :(
&lt;/h2&gt;

&lt;p&gt;Three months ago I deployed my Qwik site to Netlify. And after some while, I pushed an new, unrelated content change and the build died without touching a thing on it’s codebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Edge Functions bundling
────────────────────────────────────────────────────────────────

Packaging Edge Functions from .netlify/edge-functions directory:
 - entry.netlify-edge

[Error: ENOENT: no such file or directory, stat '/tmp/tmp-2269-6qAdCcbGEFml/qwik-city-not-found-paths.js'] {
  errno: -2,
  code: 'ENOENT',
  syscall: 'stat',
  path: '/tmp/tmp-2269-6qAdCcbGEFml/qwik-city-not-found-paths.js'
}
Node.js v22.22.1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No version bumps in &lt;code&gt;package.json&lt;/code&gt;. No config changes. The build itself (client, server) compiled cleanly. If it worked three months ago with the exact same versions, why was it broken now?&lt;/p&gt;

&lt;p&gt;I did the usual steps: Cleared cache and ran the build, failed. But build process worked fine on local machine. Upgraded all dependencies with &lt;code&gt;ncu&lt;/code&gt; to their latest versions, pushed again, still failed. Until I created a brand new Qwik hello-world project, pushed it to Netlify. &lt;em&gt;Still failed&lt;/em&gt;, that’s when I knew there must be something wrong on Netlify’s side.&lt;/p&gt;

&lt;p&gt;Searching online for &lt;code&gt;"qwik netlify build error ENOENT file not found"&lt;/code&gt; wasn't really helpful, found only one forum post describing the same issue. No replies. Auto-closed after seven days. So I posted on Netlify's answers forum and hoping someone could give a solution. A response did come eventually, but hours later. The site was down and needed to come back up ASAP, so I wasn't going to sit there refreshing a forum tab. I started digging deep.&lt;/p&gt;




&lt;h2&gt;
  
  
  The First Clue: A Missing @
&lt;/h2&gt;

&lt;p&gt;I noticed the failure was in the Edge Functions bundling step, an additional step after everything else was done. I opened the output directory on my local machine expecting to see the missing file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;.netlify/edge-functions/entry.netlify-edge/
├── @qwik-city-not-found-paths.js  ← Here it is!
├── @qwik-city-plan.js
├── @qwik-city-static-paths.js
├── entry.netlify-edge.js
└── ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's when it gets interesting, the file existed. It just had an &lt;code&gt;@&lt;/code&gt; in front of it. Yet the error occurs when attempting to call &lt;code&gt;stat&lt;/code&gt; on a file with a filename that is almost identical, except for the missing &lt;code&gt;@&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If the bundler knew this file existed, why was it looking for a file with the wrong name?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Something was stripping the &lt;code&gt;@&lt;/code&gt; somewhere in the process.&lt;/p&gt;




&lt;h2&gt;
  
  
  Following the Trail
&lt;/h2&gt;

&lt;p&gt;I pulled down the source of &lt;code&gt;@netlify/build&lt;/code&gt; from Netlify’s GitHub repo including the package &lt;code&gt;edge-bundler&lt;/code&gt;, which responsible for that Edge Functions bundling step. I followed the call chain:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;edge_functions/index.ts → bundler.ts → formats/tarball.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;tarball.ts&lt;/code&gt;, I found the function file list being built:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;listRecursively&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bundleDir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bundleDir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getUnixPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bundleDir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tarballPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gzip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the code packs bundled edge functions into a tarball for deployment. It calls &lt;code&gt;tar.create()&lt;/code&gt; with a list of filenames. And at that point, the filenames still had their &lt;code&gt;@&lt;/code&gt; prefix intact. The correct name was being passed in. So the issue wasn't in the path calculation. The &lt;code&gt;@&lt;/code&gt; was being stripped inside &lt;code&gt;tar.create()&lt;/code&gt; itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reproducing the Bug
&lt;/h2&gt;

&lt;p&gt;To confirm, I put together a minimal reproduction with some help from Claude:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;tar&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;os&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;os&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tmpDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdtempSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tmpdir&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tmpDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@test-file.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;export default 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Without fix: ENOENT — @ is stripped&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tmpDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tmp/out.tar.gz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gzip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@test-file.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And i got:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Error: ENOENT: no such file or directory, stat '.../test-file.js']
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bug was confirmed: &lt;code&gt;node-tar&lt;/code&gt; WAS doing something on filenames that result in the removal of the &lt;code&gt;@&lt;/code&gt; symbol.&lt;/p&gt;




&lt;h2&gt;
  
  
  Root Cause: bsdtar (libarchive)’s &lt;code&gt;@archive&lt;/code&gt; syntax
&lt;/h2&gt;

&lt;p&gt;After some digging online, bsdtar/libarchive has a feature where entries starting with &lt;code&gt;@&lt;/code&gt; have special meaning: &lt;code&gt;@archive.tar&lt;/code&gt; means "open that archive and include its contents". &lt;code&gt;node-tar&lt;/code&gt;, as a tar implementation, inherited this behavior. When it sees a filename starting with &lt;code&gt;@&lt;/code&gt; in the files list, it strips the &lt;code&gt;@&lt;/code&gt; symbol and tries to open the remainder as a tar archive to read entries from.&lt;/p&gt;

&lt;p&gt;So when &lt;code&gt;node-tar&lt;/code&gt; anything starts with a @ symbol:&lt;/p&gt;

&lt;p&gt;→ Interpret this as an &lt;code&gt;archive-include&lt;/code&gt; directive&lt;br&gt;
→ It tried to open &lt;code&gt;qwik-city-not-found-paths.js&lt;/code&gt; as a tar archive&lt;br&gt;
→ Run &lt;code&gt;stat&lt;/code&gt; on &lt;code&gt;qwik-city-not-found-paths.js&lt;/code&gt;&lt;br&gt;
→ ENOENT&lt;/p&gt;


&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;tarball.ts&lt;/code&gt;, where the files array is passed to &lt;code&gt;tar.create()&lt;/code&gt;, prefix each entry with &lt;code&gt;./&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;listRecursively&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bundleDir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bundleDir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getUnixPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;listRecursively&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bundleDir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bundleDir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;getUnixPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;// ← this line&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Chain Reaction
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjo4337tt672orh71yw0q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjo4337tt672orh71yw0q.png" alt="PR got merged" width="800" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After the PR was merged into &lt;code&gt;@netlify/build&lt;/code&gt;, the maintainer traced the issue further upstream and submitted a PR to &lt;code&gt;node-tar&lt;/code&gt; itself — fixing the error handling so that errors from &lt;code&gt;@&lt;/code&gt;-prefixed entries are properly catchable instead of becoming unhandled promise rejections that the caller couldn't catch.&lt;/p&gt;

&lt;p&gt;Moreover, somehow it also uncovered a CI coverage gap: the test suite for tarball handling had a describe block that was being silently skipped because of a wrong Deno version number in the test configuration. An entire set of tarball tests had been passing CI without actually running.&lt;/p&gt;

&lt;p&gt;One missing &lt;code&gt;@&lt;/code&gt;, and suddenly there were three fixes flowing in different directions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Looking Back
&lt;/h2&gt;

&lt;p&gt;Reading someone else's TypeScript to find out why my site wouldn't deploy is not something I'd done before this week. The typical workaround for this kind of issue you would find online, is to duplicate the files without the &lt;code&gt;@&lt;/code&gt;. That works. Your build passes. You move on. But somewhere the same bug is still waiting for the next person. And the next. Until someone reads the function that's eating the &lt;code&gt;@&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There's something I've been thinking about since. The experienced developers who maintain these projects, they do this kind of invisible work all the time, and they do it faster than I ever could. They trace a bug, fix it, and the result is that thousands of people simply never encounter the problem. Nobody knows it was ever there. &lt;/p&gt;

&lt;p&gt;I'm not a developer by trade. If these projects had been closed-source, my story would have ended at the forum post with no replies, waiting for someone on the other side to notice. But because the code was right there, I could pull it down and find the answer myself. A workaround solves your problem. A root cause fix solves everyone's.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Post on netlify forum: &lt;a href="https://answers.netlify.com/t/edge-functions-bundling-failed-for-qwik-enoent-no-such-file-or-directory-stat-qwik-city-not-found-paths-js-vite-7/160697" rel="noopener noreferrer"&gt;Edge Functions bundling failed for Qwik: ENOENT: no such file or directory, stat ‘@qwik-city-not-found-paths.js’ (Vite 7) - #6 by statusbot - Support - Netlify Support Forums&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;PR: netlify/build: &lt;a href="https://github.com/netlify/build/pull/6990" rel="noopener noreferrer"&gt;fix: add ./ prefix to tar entries to handle @ prefixed filenames&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Upstream: node-tar: fix from pieh:  &lt;a href="https://github.com/isaacs/node-tar/commit/01082a42c3256ca6054f9627911cce4dbfe00d92" rel="noopener noreferrer"&gt;error from @ file entry should be catchable&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Upstream: node-tar: where stripping @ symbol as directives: &lt;a href="https://github.com/isaacs/node-tar/blob/main/src/create.ts#L43" rel="noopener noreferrer"&gt;node-tar/src/create.ts at main · isaacs/node-tar&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bug reproducing repo: &lt;a href="https://github.com/osmiumsilver/demo-tar-at-file" rel="noopener noreferrer"&gt;osmiumsilver/demo-tar-at-file&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>netlify</category>
      <category>javascript</category>
      <category>troubleshooting</category>
    </item>
  </channel>
</rss>
