<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Qemu on Charlie Chiang's blog</title><link>https://blog.chlc.cc/tags/qemu/</link><description>Recent content in Qemu on Charlie Chiang's blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 08 Jun 2026 12:25:00 +0800</lastBuildDate><atom:link href="https://blog.chlc.cc/tags/qemu/index.xml" rel="self" type="application/rss+xml"/><item><title>Bundling macOS Dynamic Libraries</title><link>https://blog.chlc.cc/p/bundling-macos-dynamic-libraries/</link><pubDate>Mon, 08 Jun 2026 12:25:00 +0800</pubDate><guid>https://blog.chlc.cc/p/bundling-macos-dynamic-libraries/</guid><description>&lt;p>I recently tried to build QEMU on macOS and make the result portable enough that it could still run after uninstalling the Homebrew packages used during the build, or I can move the whole QEMU directory to another machine. I don&amp;rsquo;t want the Homebrew build because it installs too many dependencies on the system, and I want a more self-contained bundle, not a system-wide installation.&lt;/p>
&lt;p>The short version: &lt;strong>fully static linking on macOS is not really the right target&lt;/strong>. macOS binaries still dynamically link against Apple system libraries such as &lt;code>libSystem&lt;/code>. But for tools like QEMU, what I really wanted was not a “pure static binary”; I wanted a self-contained directory like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">_qemu-11.0.1/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── bin/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── qemu-img
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── qemu-system-x86_64
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── lib/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── libglib-2.0.0.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── libgobject-2.0.0.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── libintl.8.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── libpcre2-8.0.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── ...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The binaries in &lt;code>bin/&lt;/code> should load their non-system dependencies from &lt;code>../lib&lt;/code> (note that it&amp;rsquo;s relative, not absolute, so it will work even if the whole directory is moved), instead of from &lt;code>/opt/homebrew&lt;/code>.&lt;/p>
&lt;p>This post records the process, using QEMU as the example.&lt;/p>
&lt;h2 id="the-original-problem">The original problem
&lt;/h2>&lt;p>I built QEMU roughly like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># You should refer to https://wiki.qemu.org/Hosts/Mac for the latest build instructions. This is just an example.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">brew install libffi gettext glib pkg-config zstd &lt;span class="c1"># may not be the exact set of dependencies you need&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./configure --prefix&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/bin/_qemu-11.0.1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">make -j&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>sysctl -n hw.ncpu&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">make install
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The build worked. But after uninstalling some Homebrew dependencies, running &lt;code>qemu-img&lt;/code> failed:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">dyld[82783]: Library not loaded: /opt/homebrew/opt/glib/lib/libglib-2.0.0.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Referenced from: /Users/charlie/bin/_qemu-11.0.1/bin/qemu-img
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Reason: tried: &amp;#39;/opt/homebrew/opt/glib/lib/libglib-2.0.0.dylib&amp;#39; (no such file)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">zsh: abort qemu-img
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This means the installed QEMU binary still contains an absolute dependency path:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">/opt/homebrew/opt/glib/lib/libglib-2.0.0.dylib
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>We can confirm that with:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">otool -L &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$PREFIX&lt;/span>&lt;span class="s2">/bin/qemu-img&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Example output:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">qemu-img:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> /opt/homebrew/opt/glib/lib/libglib-2.0.0.dylib (compatibility version 8801.0.0, current version 8801.0.0)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> /opt/homebrew/opt/zstd/lib/libzstd.1.dylib (compatibility version 1.0.0, current version 1.5.7)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1351.0.0)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The &lt;code>/usr/lib&lt;/code> and &lt;code>/System/Library&lt;/code> entries are fine. They are macOS system libraries. The &lt;code>/opt/homebrew/...&lt;/code> entries are the ones that make the binary depend on my local Homebrew installation.&lt;/p>
&lt;h2 id="why-not-just-build-qemu-fully-static">Why not just build QEMU fully static?
&lt;/h2>&lt;p>On Linux, the natural thought is “just build a static binary”.&lt;/p>
&lt;p>On macOS, that is not usually how things work. macOS does not support the same style of fully static userland binary that Linux users often expect. You can try to statically link some third-party libraries, but the final program will still dynamically link to Apple system libraries.&lt;/p>
&lt;p>So the practical goal is:&lt;/p>
&lt;p>Bundle third-party &lt;code>.dylib&lt;/code> dependencies next to the program, and rewrite Mach-O load commands so the binary loads those local copies.&lt;/p>
&lt;p>This is similar in spirit to what many &lt;code>.app&lt;/code> bundles do, but here I wanted a plain CLI directory layout.&lt;/p>
&lt;h2 id="the-tools-involved">The tools involved
&lt;/h2>&lt;p>macOS Mach-O binaries can be inspected and patched with these tools:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">otool -L &amp;lt;file&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">install_name_tool -change &amp;lt;old&amp;gt; &amp;lt;new&amp;gt; &amp;lt;file&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">install_name_tool -id &amp;lt;new-id&amp;gt; &amp;lt;dylib&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">install_name_tool -add_rpath &amp;lt;rpath&amp;gt; &amp;lt;file&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>otool -L&lt;/code> shows dynamic library load commands.&lt;/p>
&lt;p>For example:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">otool -L &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$PREFIX&lt;/span>&lt;span class="s2">/bin/qemu-img&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>might show:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">qemu-img:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> /opt/homebrew/opt/glib/lib/libglib-2.0.0.dylib (compatibility version 8801.0.0, current version 8801.0.0)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> /opt/homebrew/opt/zstd/lib/libzstd.1.dylib (compatibility version 1.0.0, current version 1.5.7)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1351.0.0)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Those can be rewritten with:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">install_name_tool -change &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> /opt/homebrew/opt/glib/lib/libglib-2.0.0.dylib &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="s1">&amp;#39;@executable_path/../lib/libglib-2.0.0.dylib&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$PREFIX&lt;/span>&lt;span class="s2">/bin/qemu-img&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>@executable_path&lt;/code> means “the directory containing the executable being launched”.&lt;/p>
&lt;p>So it will try to find &lt;code>libglib-2.0.0.dylib&lt;/code> in &lt;code>../lib/&lt;/code> relative to the executable, no longer looking in &lt;code>/opt/homebrew&lt;/code>.&lt;/p>
&lt;h2 id="first-attempt-patch-direct-dependencies-only">First attempt: patch direct dependencies only
&lt;/h2>&lt;p>My first attempt was simple:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">otool -L &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$PREFIX&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>/bin/* &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> grep /opt/homebrew &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> sort &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> uniq &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> awk &lt;span class="s1">&amp;#39;{print $1}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>This found direct Homebrew dependencies used by QEMU binaries, such as:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">/opt/homebrew/opt/glib/lib/libgio-2.0.0.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/opt/homebrew/opt/glib/lib/libglib-2.0.0.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/opt/homebrew/opt/glib/lib/libgmodule-2.0.0.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/opt/homebrew/opt/glib/lib/libgobject-2.0.0.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/opt/homebrew/opt/zstd/lib/libzstd.1.dylib
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Then I copied those libraries into &lt;code>$PREFIX/lib&lt;/code> and patched the binaries.&lt;/p>
&lt;p>This fixed some errors, but not all of them.&lt;/p>
&lt;p>Running &lt;code>qemu-img&lt;/code> then failed with a different error:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">dyld[97126]: Library not loaded: /opt/homebrew/opt/gettext/lib/libintl.8.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Referenced from: /Users/charlie/bin/_qemu-11.0.1/lib/libglib-2.0.0.dylib
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>So I realized that it is not enough to patch only the binaries. The copied &lt;code>.dylib&lt;/code> files have their own dependencies too.&lt;/p>
&lt;p>In this case:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">qemu-img
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; libglib-2.0.0.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; libintl.8.dylib
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>libintl.8.dylib&lt;/code> was not directly referenced by &lt;code>qemu-img&lt;/code>. It was a transitive dependency of &lt;code>libglib&lt;/code>.&lt;/p>
&lt;h2 id="the-correct-model">The correct model
&lt;/h2>&lt;p>The dependency graph looks like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">bin/qemu-img
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; /opt/homebrew/opt/glib/lib/libglib-2.0.0.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; /opt/homebrew/opt/zstd/lib/libzstd.1.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">lib/libglib-2.0.0.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; /opt/homebrew/opt/gettext/lib/libintl.8.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; /opt/homebrew/opt/pcre2/lib/libpcre2-8.0.dylib
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>So the bundling process must be recursive:&lt;/p>
&lt;ol>
&lt;li>Scan Mach-O files in &lt;code>bin/&lt;/code>.&lt;/li>
&lt;li>Find its &lt;code>/opt/homebrew&lt;/code> dependencies.&lt;/li>
&lt;li>Copy those &lt;code>.dylib&lt;/code> files into &lt;code>lib/&lt;/code>.&lt;/li>
&lt;li>Patch the binaries to refer to the bundled copies (&lt;code>../lib&lt;/code>).&lt;/li>
&lt;li>Scan the copied &lt;code>.dylib&lt;/code> files.&lt;/li>
&lt;li>Copy and patch their dependencies too.&lt;/li>
&lt;li>Repeat until no &lt;code>/opt/homebrew&lt;/code> dependencies remain.&lt;/li>
&lt;/ol>
&lt;h2 id="which-paths-should-be-used">Which paths should be used?
&lt;/h2>&lt;p>For executables in &lt;code>bin/&lt;/code>, I use:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">@executable_path/../lib/libfoo.dylib
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>For dylibs inside &lt;code>lib/&lt;/code>, I use:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">@loader_path/libfoo.dylib
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;ul>
&lt;li>
&lt;p>&lt;code>@executable_path&lt;/code> is relative to the main executable being launched.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;code>@loader_path&lt;/code> is relative to the Mach-O file that is doing the loading. For dependencies between dylibs, this is what we want:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">lib/libglib-2.0.0.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; @loader_path/libintl.8.dylib
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ul>
&lt;p>Since both files are in the same &lt;code>lib/&lt;/code> directory, this resolves correctly.&lt;/p>
&lt;h2 id="the-final-bundling-script">The final bundling script
&lt;/h2>&lt;p>I ended up writing a small Python script called &lt;code>macho-bundle-deps&lt;/code>. You can find it here: &lt;a class="link" href="https://github.com/charlie0129/dotfiles/blob/f15791054e517f6b5afc892312f1db73f331d475/bin/darwin/macho-bundle-deps" target="_blank" rel="noopener"
>https://github.com/charlie0129/dotfiles/blob/f15791054e517f6b5afc892312f1db73f331d475/bin/darwin/macho-bundle-deps&lt;/a>. This is a permanent link to a specific commit, so you may want to check the latest version in the repository.&lt;/p>
&lt;p>Usage:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">PREFIX&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/bin/_qemu-11.0.1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">macho-bundle-deps &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --lib-dir &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$PREFIX&lt;/span>&lt;span class="s2">/lib&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --prefix /opt/homebrew &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$PREFIX&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>/bin
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Or, from inside the QEMU prefix:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/bin/_qemu-11.0.1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">macho-bundle-deps --lib-dir lib bin
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>The script does the following:&lt;/p>
&lt;ul>
&lt;li>finds Mach-O files from the input paths&lt;/li>
&lt;li>scans their dependencies with &lt;code>otool -L&lt;/code>&lt;/li>
&lt;li>copies matching dependencies into &lt;code>--lib-dir&lt;/code>&lt;/li>
&lt;li>patches executable dependencies to &lt;code>@executable_path/../lib/...&lt;/code>&lt;/li>
&lt;li>patches bundled dylib dependencies to &lt;code>@loader_path/...&lt;/code>&lt;/li>
&lt;li>patches copied dylib IDs with &lt;code>install_name_tool -id&lt;/code>&lt;/li>
&lt;li>recursively processes newly copied dylibs&lt;/li>
&lt;li>reports an error if matching absolute dependency paths remain&lt;/li>
&lt;/ul>
&lt;h2 id="verifying-the-result">Verifying the result
&lt;/h2>&lt;p>After running the bundler, I verify that no Homebrew paths remain:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">otool -L &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$PREFIX&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>/bin/* &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$PREFIX&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>/lib/*.dylib &lt;span class="p">|&lt;/span> grep /opt/homebrew
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Now I can uninstall the dependencies from Homebrew, and the bundled QEMU still works:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">brew uninstall libffi gettext glib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">qemu-img --help
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="notes-and-limitations">Notes and limitations
&lt;/h2>&lt;p>This is not the same as producing a fully static binary.&lt;/p>
&lt;p>The result still depends on macOS system libraries, which is normal:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">/usr/lib/libSystem.B.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/System/Library/Frameworks/...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>It also does not magically make a binary portable across all macOS versions or CPU architectures. A binary built on Apple Silicon is still an arm64 Mach-O binary unless built otherwise.&lt;/p>
&lt;p>This approach is mainly useful for making a local CLI tool directory self-contained with respect to Homebrew dependencies.&lt;/p>
&lt;h2 id="why-copying-symlinks-should-be-avoided">Why copying symlinks should be avoided
&lt;/h2>&lt;p>Homebrew often has dylib symlinks like:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">libintl.dylib -&amp;gt; libintl.8.dylib
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>When bundling, prefer copying the symlink target, not the symlink itself.&lt;/p>
&lt;p>In shell, that means:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">cp -L source.dylib &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$PREFIX&lt;/span>&lt;span class="s2">/lib/&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>In Python, &lt;code>shutil.copy2(..., follow_symlinks=True)&lt;/code> does the same thing.&lt;/p>
&lt;p>This avoids ending up with a bundled symlink pointing to a non-existent file.&lt;/p>
&lt;h2 id="final-layout">Final layout
&lt;/h2>&lt;p>After bundling, the QEMU directory looks like this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">_qemu-11.0.1/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── bin/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── qemu-img
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── qemu-io
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── qemu-nbd
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── qemu-system-aarch64
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── qemu-system-x86_64
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── lib/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── libffi.8.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── libgio-2.0.0.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── libglib-2.0.0.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── libgmodule-2.0.0.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── libgobject-2.0.0.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── libintl.8.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── libpcre2-8.0.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── libzstd.1.dylib
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Now &lt;code>qemu-img&lt;/code> no longer cares whether Homebrew’s &lt;code>glib&lt;/code>, &lt;code>gettext&lt;/code>, &lt;code>pcre2&lt;/code>, or &lt;code>zstd&lt;/code> packages are installed.&lt;/p>
&lt;h2 id="conclusion">Conclusion
&lt;/h2>&lt;p>The main lesson is that macOS dependency bundling is graph traversal, not a one-shot patch.&lt;/p>
&lt;p>Patching only the top-level binaries is not enough. You also need to patch the dylibs that you copied, and then patch the dylibs that those dylibs depend on.&lt;/p>
&lt;p>The final strategy is:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">scan -&amp;gt; copy -&amp;gt; patch -&amp;gt; scan copied dylibs -&amp;gt; repeat
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>For executables:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">/opt/homebrew/.../libfoo.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; @executable_path/../lib/libfoo.dylib
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>For bundled dylibs:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">/opt/homebrew/.../libbar.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; @loader_path/libbar.dylib
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>And for the dylib’s own install name:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">/opt/homebrew/.../libfoo.dylib
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -&amp;gt; @loader_path/libfoo.dylib
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div></description></item></channel></rss>