<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Packaging on PHP Boy Scout</title><link>https://phpboyscout.uk/tags/packaging/</link><description>Recent content in Packaging on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Sun, 12 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://phpboyscout.uk/tags/packaging/index.xml" rel="self" type="application/rss+xml"/><item><title>Why my photo app won't build for Windows</title><link>https://phpboyscout.uk/why-my-photo-app-wont-build-for-windows/</link><pubDate>Sun, 12 Jul 2026 00:00:00 +0000</pubDate><guid>https://phpboyscout.uk/why-my-photo-app-wont-build-for-windows/</guid><description>&lt;img src="https://phpboyscout.uk/why-my-photo-app-wont-build-for-windows/cover-why-my-photo-app-wont-build-for-windows.png" alt="Featured image of post Why my photo app won't build for Windows" /&gt;&lt;p&gt;I was wiring up the release build for krites, the wedding-photo culler I&amp;rsquo;ve been &lt;a class="link" href="https://phpboyscout.uk/i-built-my-wife-a-judge/" &gt;building for my wife&lt;/a&gt;, and I went to add Windows to the list of targets. It felt like the responsible thing. You write software, you ship it for the three big desktops, that&amp;rsquo;s just tidy. So I added &lt;code&gt;windows&lt;/code&gt; to the build config, kicked off a cross-compile from my Linux box, and watched it fall over before it had really started.&lt;/p&gt;
&lt;p&gt;Now, the sensible engineer&amp;rsquo;s instinct at that point is to roll up the sleeves and fix it. Mine was too, for about five minutes&amp;hellip; then I read the actual error, thought about who was ever going to run this thing, and did the opposite. I deleted Windows and moved on.&lt;/p&gt;
&lt;h2 id="the-binary-with-no-c-in-it"&gt;The binary with no C in it
&lt;/h2&gt;&lt;p&gt;To explain why the build died, I have to back up to a decision that felt clever at the time.&lt;/p&gt;
&lt;p&gt;krites does some machine learning. Not for the culling itself, which is &lt;a class="link" href="https://phpboyscout.uk/no-ai-in-my-photo-culler/" &gt;deliberately model-free arithmetic&lt;/a&gt;, but for the finer judgements: is a subject mid-blink, are they looking at the camera. Those run on ONNX Runtime, Microsoft&amp;rsquo;s inference engine, which is a big lump of C++ that lives in a shared library.&lt;/p&gt;
&lt;p&gt;The usual way to call a C library from Go is CGO. It works, but it comes with a tax: the moment you switch CGO on, building your program means having a C compiler and the right headers for every platform you target. Cross-compiling from one machine to all the others, which is the whole convenience of Go, stops being free. You end up maintaining a little zoo of toolchains just to produce binaries.&lt;/p&gt;
&lt;p&gt;So I went the other way. There&amp;rsquo;s a lovely library called &lt;a class="link" href="https://github.com/ebitengine/purego" target="_blank" rel="noopener"
 &gt;purego&lt;/a&gt; that lets you call into a C shared library with CGO switched off entirely. It does the dynamic loading itself, in pure Go, and hands you back function pointers. The ONNX binding krites uses, &lt;code&gt;onnxruntime-purego&lt;/code&gt;, is built on it. The upshot is a binary compiled with &lt;code&gt;CGO_ENABLED=0&lt;/code&gt;: no C compiler in the build, no headers, cross-compile to anything from my laptop. The provisioner that fetches the runtime library is &lt;a class="link" href="https://gitlab.com/phpboyscout/krites/-/blob/a02f678/pkg/onnxruntime/onnxruntime.go#L1-L45" target="_blank" rel="noopener"
 &gt;ordinary Go&lt;/a&gt;, &lt;code&gt;net/http&lt;/code&gt; and &lt;code&gt;archive/tar&lt;/code&gt;, nothing exotic.&lt;/p&gt;
&lt;p&gt;Pure Go, no CGO, builds everywhere. You can probably see where this is going&amp;hellip;&lt;/p&gt;
&lt;h2 id="the-trick-that-doesnt-cross-the-channel"&gt;The trick that doesn&amp;rsquo;t cross the Channel
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the bit I hadn&amp;rsquo;t thought hard enough about. &lt;em&gt;How&lt;/em&gt; does purego load a shared library without CGO? It calls &lt;code&gt;dlopen&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dlopen&lt;/code&gt; is a Unix system call. It&amp;rsquo;s how a running program reaches out and pulls in a &lt;code&gt;.so&lt;/code&gt; or a &lt;code&gt;.dylib&lt;/code&gt; at runtime, the thing CGO normally does for you behind the scenes. purego just does it directly. And that&amp;rsquo;s the whole trick: no C compiler needed, because the loading happens through a system call that&amp;rsquo;s already sitting there in the OS.&lt;/p&gt;
&lt;p&gt;Except it isn&amp;rsquo;t there on Windows. Windows has its own mechanism for the same job, &lt;code&gt;LoadLibrary&lt;/code&gt;, with a completely different shape. So purego&amp;rsquo;s &lt;code&gt;dlopen&lt;/code&gt; and the flags that go with it (&lt;code&gt;RTLD_NOW&lt;/code&gt;, &lt;code&gt;RTLD_GLOBAL&lt;/code&gt;, if you&amp;rsquo;ve ever squinted at a Unix man page) simply don&amp;rsquo;t exist when you compile for Windows. They&amp;rsquo;re guarded behind a build constraint that reads, in effect, &amp;ldquo;Linux or macOS or the other BSDs, and nothing else&amp;rdquo;. Target Windows and those symbols vanish. The binding calls a function that, on that platform, was never compiled in.&lt;/p&gt;
&lt;p&gt;My favourite detail is that the binding &lt;em&gt;looks&lt;/em&gt; like it supports Windows right up until the moment it can&amp;rsquo;t. Its library-name helper has a branch for it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;switch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;darwin&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;libonnxruntime.dylib&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;linux&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;libonnxruntime.so&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;windows&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;onnxruntime.dll&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It knows the DLL is called &lt;code&gt;onnxruntime.dll&lt;/code&gt;. It&amp;rsquo;s dressed for the party. Then two lines later it goes to actually open the thing with &lt;code&gt;dlopen&lt;/code&gt;, the linker looks for a symbol that isn&amp;rsquo;t there for this target, and the whole build stops. The Windows case is code that can never run, because it can never compile.&lt;/p&gt;
&lt;p&gt;So this was never a bug waiting for me to be clever enough to fix. A CGO-free binding buys you a great deal, but it buys it &lt;em&gt;by being Unix&lt;/em&gt;. The portability I was so chuffed with was POSIX portability, and Windows was never inside the fence. I&amp;rsquo;d picked the option that looked like it built everywhere and it still had a border on it.&lt;/p&gt;
&lt;h2 id="knowing-where-the-software-actually-lives"&gt;Knowing where the software actually lives
&lt;/h2&gt;&lt;p&gt;Which left me with the interesting question, and it wasn&amp;rsquo;t &amp;ldquo;how do I make Windows work&amp;rdquo;. It was &amp;ldquo;does anyone need it to&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;krites is for Hailey, my wife. She&amp;rsquo;s a photographer, and she works on a Mac, because that&amp;rsquo;s what photographers do. The whole plan for the ML side leans into that: the fast path is Apple&amp;rsquo;s own CoreML running on the M-series chip, the dylib bundled right beside the app. macOS isn&amp;rsquo;t a platform I tolerate here, it&amp;rsquo;s the one I&amp;rsquo;m building &lt;em&gt;for&lt;/em&gt;. Linux is the other target, and it earns its place for a different reason, it&amp;rsquo;s where the CI runs and where a headless batch job would live. Neither of those is Windows. Windows was on the list purely because three feels like a rounder number than two.&lt;/p&gt;
&lt;p&gt;So the fix was a diff that removed a line. The &lt;a class="link" href="https://gitlab.com/phpboyscout/krites/-/blob/a02f678/.goreleaser.yaml#L9-L22" target="_blank" rel="noopener"
 &gt;release config&lt;/a&gt; now lists &lt;code&gt;linux&lt;/code&gt; and &lt;code&gt;darwin&lt;/code&gt; and stops there, with a comment for the next person (me, in six months, having forgotten all of this) explaining why the obvious third entry isn&amp;rsquo;t obvious at all.&lt;/p&gt;
&lt;p&gt;I could have chased it. There are ways: shim the loader, find a binding that speaks &lt;code&gt;LoadLibrary&lt;/code&gt;, turn CGO back on for the Windows build alone and hand-feed it a toolchain. Every one of them is real work in service of a platform my one and only user will never open the app on. That&amp;rsquo;s effort spent making a number rounder.&lt;/p&gt;
&lt;p&gt;A build target isn&amp;rsquo;t a box you tick to look thorough. It&amp;rsquo;s a claim about where your software actually lives, and krites lives on a photographer&amp;rsquo;s Mac. Once I&amp;rsquo;d said that out loud, the Windows build stopped being a problem to solve. It was a line I got to delete.&lt;/p&gt;</description></item></channel></rss>