Describe the bug
tauri-cli's get_binaries() runs in two stages when enumerating binaries to bundle. Stage 1 reads [[bin]] entries from Cargo.toml and correctly skips entries whose required-features aren't in the build's enabled feature set. Stage 2 walks src-tauri/src/bin/ from disk and adds back every name not already in the list — with no required-features filter. The result: a [[bin]] correctly gated on required-features = ["dev-only-feature"] is still picked up by the bundler if its source happens to live under src/bin/, and the bundler then fails trying to copy a binary cargo never built.
This breaks the standard Cargo convention of putting developer-tooling bins under src/bin/ and gating them on a feature so they don't ship in release. The release-mode failure surfaces as a confusing "file not found" copy error in the bundler that doesn't point to the disk-scan logic.
Reproduction
A Tauri 2 app with two bins, the second gated on a dev-only feature:
# src-tauri/Cargo.toml
[[bin]]
name = "my-app"
path = "src/main.rs"
[[bin]]
name = "generate-bindings"
path = "src/bin/generate-bindings.rs"
required-features = ["dev-tools"]
[features]
dev-tools = []
src/bin/generate-bindings.rs:
fn main() { println!("dev tool"); }
Run cargo tauri build (release, no --features dev-tools):
Bundling my-app.app (...)
Error failed to bundle project Failed to copy binary from
".../release/generate-bindings": `".../release/generate-bindings" does not exist`
Cargo correctly didn't build generate-bindings because dev-tools wasn't enabled. The bundler tries to copy it anyway because Stage 2's disk scan in crates/tauri-cli/src/interface/rust.rs get_binaries() (around lines 963–1002) found the source file and added the bin back to the list.
Workaround that works: move the source out of src/bin/ to e.g. dev-bin/ and update [[bin]] path =. Stage 2 only walks src/bin/, so it finds nothing; Stage 1's required-features check is the only gate that runs. Fragile (relies on the disk-scan path not getting broadened later) and breaks the Cargo convention.
Expected behavior
Stage 2's disk scan should consult the manifest before re-adding a discovered name. If a [[bin]] entry exists for that name with required-features not satisfied by the current build, skip it — same gate as Stage 1.
Pseudocode for the fix:
// In Stage 2, after detecting a bin name from src/bin/<name>.rs:
let manifest_entry = manifest_bins.iter().find(|b| b.name == name);
if let Some(entry) = manifest_entry {
if !entry.required_features.iter().all(|f| options.features.contains(f)) {
continue;
}
}
This preserves auto-discovery for bins without a Cargo.toml entry but stops re-enabling explicitly gated ones.
Alternative (less invasive): expose a bundle.bin allowlist in tauri.conf.json so users can declare "only bundle these names" without depending on auto-discovery to do the right thing. Tracked at #9180.
Full tauri info output
[⚠] Environment
- OS: Mac OS 26.3.1 arm64 (X64)
✔ Xcode Command Line Tools: installed
✔ rustc: 1.93.0 (254b59607 2026-01-19) (Homebrew)
✔ cargo: 1.93.0 (Homebrew)
- node: 23.7.0
- pnpm: 10.29.3
- npm: 10.9.2
- bun: 1.3.9
[-] Packages
- tauri 🦀: 2.10.2, (outdated, latest: 2.11.0)
- tauri-build 🦀: 2.5.5, (outdated, latest: 2.6.0)
- wry 🦀: 0.54.2, (outdated, latest: 0.55.0)
- tao 🦀: 0.34.5, (outdated, latest: 0.35.0)
- @tauri-apps/api : 2.10.1 (outdated, latest: 2.11.0)
- @tauri-apps/cli : 2.8.0 (outdated, latest: 2.11.0)
[-] Plugins
- tauri-plugin-shell 🦀: 2.3.5
- tauri-plugin-dialog 🦀: 2.6.0
- tauri-plugin-updater 🦀: 2.10.0
- tauri-plugin-fs 🦀: 2.5.0
- tauri-plugin-single-instance 🦀: 2.4.0
- tauri-plugin-opener 🦀: 2.5.3
- tauri-plugin-deep-link 🦀: 2.4.7
- tauri-plugin-global-shortcut 🦀: 2.3.1
- tauri-plugin-log 🦀: 2.8.0
- tauri-plugin-process 🦀: 2.3.1
[-] App
- build-type: bundle
- CSP: unset
- frontendDist: ../dist
- devUrl: http://localhost:1420/
- bundler: Rollup
(Disk-scan logic is platform-independent — verified against current tauri-cli source on dev as of 2026-05-02.)
Stack trace
Compiling moss v0.6.4 (.../src-tauri)
Finished `release-optimized` profile [optimized] target(s) in 8m 14s
Built application at: .../target/universal-apple-darwin/release-optimized/moss
Bundling moss.app (.../target/universal-apple-darwin/release-optimized/bundle/macos/moss.app)
failed to bundle project Failed to copy binary from
".../target/universal-apple-darwin/release-optimized/generate-bindings":
`".../target/universal-apple-darwin/release-optimized/generate-bindings" does not exist`
Error failed to bundle project Failed to copy binary from
".../target/universal-apple-darwin/release-optimized/generate-bindings":
`".../target/universal-apple-darwin/release-optimized/generate-bindings" does not exist`
##[error]Process completed with exit code 1.
Additional context
Hit on three consecutive release attempts of an open-source Tauri 2 app before tracing it to tauri-cli. Two prior attempted hotfixes failed because they assumed required-features alone was the gate:
- Gating
main() behind cfg(debug_assertions) — bin compiled in release but produced no useful binary; bundler still tried to copy it (or rather, it was built so the bundler "worked" but was including a useless artifact).
- Adding
required-features = ["dev-tools"] to the [[bin]] — Stage 1 honored it, Stage 2 didn't; bundle still failed.
The eventually working workaround was to move the file out of src/bin/ entirely. That goes against the standard Cargo idiom for developer-tooling bins.
Related: #9180 (open feature request for an explicit bin allowlist; would also solve this if implemented).
Describe the bug
tauri-cli'sget_binaries()runs in two stages when enumerating binaries to bundle. Stage 1 reads[[bin]]entries fromCargo.tomland correctly skips entries whoserequired-featuresaren't in the build's enabled feature set. Stage 2 walkssrc-tauri/src/bin/from disk and adds back every name not already in the list — with norequired-featuresfilter. The result: a[[bin]]correctly gated onrequired-features = ["dev-only-feature"]is still picked up by the bundler if its source happens to live undersrc/bin/, and the bundler then fails trying to copy a binary cargo never built.This breaks the standard Cargo convention of putting developer-tooling bins under
src/bin/and gating them on a feature so they don't ship in release. The release-mode failure surfaces as a confusing "file not found" copy error in the bundler that doesn't point to the disk-scan logic.Reproduction
A Tauri 2 app with two bins, the second gated on a dev-only feature:
src/bin/generate-bindings.rs:Run
cargo tauri build(release, no--features dev-tools):Cargo correctly didn't build
generate-bindingsbecausedev-toolswasn't enabled. The bundler tries to copy it anyway because Stage 2's disk scan incrates/tauri-cli/src/interface/rust.rsget_binaries()(around lines 963–1002) found the source file and added the bin back to the list.Workaround that works: move the source out of
src/bin/to e.g.dev-bin/and update[[bin]] path =. Stage 2 only walkssrc/bin/, so it finds nothing; Stage 1'srequired-featurescheck is the only gate that runs. Fragile (relies on the disk-scan path not getting broadened later) and breaks the Cargo convention.Expected behavior
Stage 2's disk scan should consult the manifest before re-adding a discovered name. If a
[[bin]]entry exists for that name withrequired-featuresnot satisfied by the current build, skip it — same gate as Stage 1.Pseudocode for the fix:
This preserves auto-discovery for bins without a
Cargo.tomlentry but stops re-enabling explicitly gated ones.Alternative (less invasive): expose a
bundle.binallowlist intauri.conf.jsonso users can declare "only bundle these names" without depending on auto-discovery to do the right thing. Tracked at #9180.Full
tauri infooutput(Disk-scan logic is platform-independent — verified against current
tauri-clisource ondevas of 2026-05-02.)Stack trace
Additional context
Hit on three consecutive release attempts of an open-source Tauri 2 app before tracing it to
tauri-cli. Two prior attempted hotfixes failed because they assumedrequired-featuresalone was the gate:main()behindcfg(debug_assertions)— bin compiled in release but produced no useful binary; bundler still tried to copy it (or rather, it was built so the bundler "worked" but was including a useless artifact).required-features = ["dev-tools"]to the[[bin]]— Stage 1 honored it, Stage 2 didn't; bundle still failed.The eventually working workaround was to move the file out of
src/bin/entirely. That goes against the standard Cargo idiom for developer-tooling bins.Related: #9180 (open feature request for an explicit bin allowlist; would also solve this if implemented).