<?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: UBro-dev</title>
    <description>The latest articles on DEV Community by UBro-dev (@ubrodev).</description>
    <link>https://dev.to/ubrodev</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4226941%2Fe9835cfe-c03b-472a-8ada-22fb44b7ae53.png</url>
      <title>DEV Community: UBro-dev</title>
      <link>https://dev.to/ubrodev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ubrodev"/>
    <language>en</language>
    <item>
      <title>I found a bug in Tauri's universal macOS builds (and my CI lied to me)</title>
      <dc:creator>UBro-dev</dc:creator>
      <pubDate>Wed, 04 Nov 2026 18:30:57 +0000</pubDate>
      <link>https://dev.to/ubrodev/i-found-a-bug-in-tauris-universal-macos-builds-and-my-ci-lied-to-me-31f1</link>
      <guid>https://dev.to/ubrodev/i-found-a-bug-in-tauris-universal-macos-builds-and-my-ci-lied-to-me-31f1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; 🧵 A Tauri app with two &lt;code&gt;[[bin]]&lt;/code&gt; targets can't be bundled as a universal macOS app: only the main binary gets merged with &lt;code&gt;lipo&lt;/code&gt;. And &lt;code&gt;tauri-action&lt;/code&gt; will happily finish green after uploading nothing. Below: why it happens, workarounds for both, and the issue I filed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I maintain &lt;a href="https://github.com/UBro-dev/stacker-pulse" rel="noopener noreferrer"&gt;StackerPulse&lt;/a&gt;, a small open-source desktop app built with &lt;strong&gt;Rust&lt;/strong&gt; and &lt;strong&gt;Tauri v2&lt;/strong&gt;. It ships for Windows, Linux and macOS from a single GitHub Actions workflow.&lt;/p&gt;

&lt;p&gt;Getting the macOS release out took two surprises:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🍎 &lt;strong&gt;#1&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Failed to copy binary … does not exist&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;universal build merges only the main binary&lt;/td&gt;
&lt;td&gt;make extra bins opt-in with &lt;code&gt;required-features&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✅ &lt;strong&gt;#2&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;CI is green, release has no installers&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tauri-action&lt;/code&gt; silently skips uploads without a release id&lt;/td&gt;
&lt;td&gt;pass &lt;code&gt;tagName&lt;/code&gt;, verify the assets&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🍎 Surprise 1: "file does not exist" in a universal build
&lt;/h2&gt;

&lt;p&gt;The app crate has two binaries: the app itself and a small command-line scanner.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="k"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="k"&gt;]]&lt;/span&gt;
&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"stacker-pulse"&lt;/span&gt;
&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"src/main.rs"&lt;/span&gt;

&lt;span class="k"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="k"&gt;]]&lt;/span&gt;
&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"seedscan-cli"&lt;/span&gt;
&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"src/seedscan_cli.rs"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Windows and Linux bundled fine. The universal macOS build (&lt;code&gt;--target universal-apple-darwin&lt;/code&gt;) compiled for &lt;strong&gt;both architectures&lt;/strong&gt; and then died at bundling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Built application at: .../target/universal-apple-darwin/release/stacker-pulse
failed to bundle project: Failed to copy binary from
".../target/universal-apple-darwin/release/seedscan-cli": does not exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🔍 What's going on
&lt;/h3&gt;

&lt;p&gt;I read the tauri-cli source (2.12.4). Two things don't line up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🧩 In &lt;code&gt;crates/tauri-cli/src/interface/rust/desktop.rs&lt;/code&gt;, the universal branch of &lt;code&gt;build()&lt;/code&gt; builds both architectures, then runs &lt;strong&gt;one&lt;/strong&gt; &lt;code&gt;lipo -create&lt;/code&gt; command, for the &lt;strong&gt;main binary only&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;📦 The bundler takes its list from &lt;code&gt;get_binaries()&lt;/code&gt; in &lt;code&gt;crates/tauri-cli/src/interface/rust.rs&lt;/code&gt;, which returns &lt;strong&gt;every&lt;/strong&gt; &lt;code&gt;[[bin]]&lt;/code&gt; whose &lt;code&gt;required-features&lt;/code&gt; are enabled.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;The extra binary is compiled for each architecture but never merged into &lt;code&gt;target/universal-apple-darwin/release/&lt;/code&gt;, so the bundler goes looking for a file that was never created.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  🛠️ Workaround
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;get_binaries()&lt;/code&gt; skips binaries whose &lt;code&gt;required-features&lt;/code&gt; aren't enabled. Make the extra binary opt-in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="k"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="k"&gt;]]&lt;/span&gt;
&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"seedscan-cli"&lt;/span&gt;
&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"src/seedscan_cli.rs"&lt;/span&gt;
&lt;span class="n"&gt;required-features&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"cli"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;cli&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tauri build&lt;/code&gt; enables no features, so the app bundles cleanly on every OS. The CLI is built separately, once per architecture on macOS, and merged by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo build &lt;span class="nt"&gt;--release&lt;/span&gt; &lt;span class="nt"&gt;--features&lt;/span&gt; cli &lt;span class="nt"&gt;--bin&lt;/span&gt; seedscan-cli &lt;span class="nt"&gt;--target&lt;/span&gt; aarch64-apple-darwin
cargo build &lt;span class="nt"&gt;--release&lt;/span&gt; &lt;span class="nt"&gt;--features&lt;/span&gt; cli &lt;span class="nt"&gt;--bin&lt;/span&gt; seedscan-cli &lt;span class="nt"&gt;--target&lt;/span&gt; x86_64-apple-darwin
lipo &lt;span class="nt"&gt;-create&lt;/span&gt; &lt;span class="nt"&gt;-output&lt;/span&gt; seedscan-cli &lt;span class="se"&gt;\&lt;/span&gt;
  target/aarch64-apple-darwin/release/seedscan-cli &lt;span class="se"&gt;\&lt;/span&gt;
  target/x86_64-apple-darwin/release/seedscan-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ Pass &lt;code&gt;--features cli&lt;/code&gt; to &lt;code&gt;cargo test&lt;/code&gt; and &lt;code&gt;cargo clippy&lt;/code&gt; too, or the CLI's tests &lt;strong&gt;silently stop running&lt;/strong&gt;. This workaround relies on tauri-cli respecting &lt;code&gt;required-features&lt;/code&gt;, which is reliable since &lt;strong&gt;2.11.3&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  📮 Reported upstream
&lt;/h3&gt;

&lt;p&gt;There was no existing report for this case: older issues are about &lt;code&gt;externalBin&lt;/code&gt; sidecars, a different mechanism. So I filed &lt;strong&gt;&lt;a href="https://github.com/tauri-apps/tauri/issues/16460" rel="noopener noreferrer"&gt;tauri-apps/tauri#16460&lt;/a&gt;&lt;/strong&gt; with a minimal reproduction and a suggested fix: run &lt;code&gt;lipo&lt;/code&gt; for every binary returned by &lt;code&gt;get_binaries()&lt;/code&gt;, not just the main one.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;At the time of writing, the issue is open and waiting for triage.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Surprise 2: green CI, empty release
&lt;/h2&gt;

&lt;p&gt;With bundling fixed, the release workflow finished &lt;strong&gt;green&lt;/strong&gt;. The draft release had the CLI archives and checksums, and &lt;strong&gt;no installers at all&lt;/strong&gt;. 🙃&lt;/p&gt;

&lt;p&gt;The workflow creates a draft release first and passes its id to &lt;code&gt;tauri-apps/tauri-action&lt;/code&gt;. My step that looked up the id returned an empty string. Given no release id and no tag name, tauri-action builds everything, logs this, and &lt;strong&gt;exits successfully&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No releaseId or tagName provided, skipping all uploads...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🛠️ Two fixes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Give tauri-action the tag as a fallback&lt;/strong&gt;, so it can find the draft on its own, and make the id lookup fail loudly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tauri-apps/tauri-action@&amp;lt;pinned-sha&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;releaseId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ needs.create-release.outputs.release_id }}&lt;/span&gt;
    &lt;span class="na"&gt;tagName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.ref_name }}&lt;/span&gt;
    &lt;span class="na"&gt;releaseDraft&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Check the result, not the exit code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;The installers really are on the draft&lt;/span&gt;
  &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;GH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.token }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;case "$RUNNER_OS" in&lt;/span&gt;
      &lt;span class="s"&gt;Windows) want='\.msi$' ;;&lt;/span&gt;
      &lt;span class="s"&gt;macOS)   want='\.dmg$' ;;&lt;/span&gt;
      &lt;span class="s"&gt;*)       want='\.AppImage$' ;;&lt;/span&gt;
    &lt;span class="s"&gt;esac&lt;/span&gt;
    &lt;span class="s"&gt;gh release view "$TAG" --json assets --jq '.assets[].name' &amp;gt; assets.txt&lt;/span&gt;
    &lt;span class="s"&gt;grep -Eq "$want" assets.txt || { echo "::error::No installer was attached"; exit 1; }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next run attached every installer, and a missing one now turns the build &lt;strong&gt;red&lt;/strong&gt; instead of quietly shipping an empty release. 🎉&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;📖 &lt;strong&gt;When a tool fails strangely, read its source.&lt;/strong&gt; The answer to surprise 1 was in about ten lines of tauri-cli.&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;A green check means "the steps ran", not "the job is done".&lt;/strong&gt; Add a step that verifies what you actually care about.&lt;/li&gt;
&lt;li&gt;📮 &lt;strong&gt;Report it upstream, even with a workaround in hand.&lt;/strong&gt; A minimal reproduction and a pointer to the code takes an hour and saves the next person a day.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;The project, the workflow and the full changelog are on &lt;strong&gt;&lt;a href="https://github.com/UBro-dev/stacker-pulse" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/strong&gt;. If you've hit something similar with Tauri or &lt;code&gt;tauri-action&lt;/code&gt;, I'd love to hear about it in the comments 👇&lt;/p&gt;

</description>
      <category>rust</category>
      <category>programming</category>
      <category>opensource</category>
      <category>github</category>
    </item>
  </channel>
</rss>
