build.rs

Understand the role of the build script in a Tauri project, how it integrates with Cargo, and how to customize it safely.

Every Tauri project that uses the default scaffolding contains a small file named build.rs inside the src-tauri/ directory. It sits at the root of the Rust crate, right next to Cargo.toml, and it is part of Cargo’s build system — not something Tauri invented. In Tauri v2, this file is the bridge that hooks the Rust backend into the rest of the toolchain before the final binary is produced.

What build.rs Actually Is

A build.rs file is a Cargo build script. It is a Rust program that Cargo compiles and executes before it compiles the rest of your crate. Its job is to do preparatory work: generate code, set environment variables, compile native dependencies, or validate the environment.

In a Tauri project, the build script exists to prepare the Tauri-specific configuration and permissions so that the Rust backend can embed them at compile time. Without it, your src-tauri/ crate would not know about your app’s window settings, its allowed commands, or its bundle identifier until runtime — which is far too late.

You can think of a build script as a setup phase that runs once per build (or when something it depends on changes). It has no access to your main crate’s code, and it runs with its own set of dependencies declared under [build-dependencies] in Cargo.toml.

The Default build.rs in a Tauri Project

A freshly scaffolded Tauri v2 project gives you a build script that is deceptively small:

// src-tauri/build.rs
fn main() {
    tauri_build::build()
}

The function tauri_build::build() comes from the tauri-build crate, which is automatically added to your Cargo.toml as a build dependency:

# src-tauri/Cargo.toml (build dependency section)
[build-dependencies]
tauri-build = { version = "2", features = [] }

Calling tauri_build::build() is not just a placeholder. It performs multiple critical steps before the Rust compiler touches your application code. The next section details what actually happens inside that call.

Correct setup:

If your build.rs contains exactly the line tauri_build::build() and the tauri-build crate is present in [build-dependencies], your Tauri build integration is correctly configured out of the box.

How tauri_build::build() Works

When Cargo runs this build script, the tauri_build::build() function does several things silently:

  1. Reads and validates tauri.conf.json.
    The build script parses your main configuration file. If it contains invalid JSON, missing required fields, or conflicting options, the build will fail with a descriptive error. This moves a whole class of configuration mistakes from runtime to compile time.

  2. Processes capabilities and permissions.
    In Tauri v2, security is enforced through capability files located in src-tauri/capabilities/. The build script collects all enabled permissions and generates Rust code that embeds them into the binary. This means the permissions your app uses are locked in at build time, not decided dynamically.

  3. Generates code and writes it to OUT_DIR.
    Cargo provides a special environment variable OUT_DIR that points to a temporary directory where build scripts are allowed to write files. tauri_build::build() creates Rust source files there containing constants, configuration structures, and permission tables. Your main crate includes these generated files via the include! macro that tauri itself calls under the hood.

  4. Sets compile-time environment variables.
    The script sets variables like TAURI_ENV_DEBUG or TAURI_ENV_TARGET_OS so that conditional compilation can react to the current platform or build mode without fragile cfg! checks for Tauri-specific logic.

  5. Emits rerun-if-changed directives.
    Cargo tracks which files a build script declares as “watched.” If any of those files change, Cargo re-runs the build script on the next build. By default, tauri_build::build() watches tauri.conf.json, the capabilities/ directory, and a few other Tauri-related paths. This ensures that a config change always triggers a recompilation of the generated code.

Behind the scenes:

The generated code ends up in a path like target/debug/build/tauri-app-<hash>/out/. You can inspect it with cargo build -vv, but you never need to edit or commit it — it is regenerated on every relevant change.

Customizing the Build Script

The default behavior covers all standard Tauri applications. You only need to modify build.rs if your project has additional build-time requirements that Cargo would otherwise ignore.

Adding Extra File Watchers

The most common customization is telling Cargo to re-run the build script when files outside the standard Tauri set change. A practical scenario: you have a local Rust library living outside src-tauri/ that your backend depends on. By default, Cargo does not watch that library for changes because it is not part of the Tauri crate’s source tree.

You can fix that by appending cargo: directives before calling tauri_build::build():

// src-tauri/build.rs
fn main() {
    // Watch a sibling crate for changes.
    println!("cargo:rerun-if-changed=../core/my-library");
    // Watch a custom JSON config file.
    println!("cargo:rerun-if-changed=config.json");
    tauri_build::build()
}

Each println! line that starts with cargo: is interpreted by Cargo as an instruction. rerun-if-changed tells Cargo: “If this file or any file inside this directory changes, mark the build script as outdated and re-run it.”

This mechanism ensures that editing your local library or a custom configuration file triggers a fresh generation of the Tauri bindings — and therefore a full rebuild of the Rust backend.

Not a dev‑server watcher:

These directives affect Cargo’s incremental compilation cache, not the file watcher that tauri dev uses for hot-reloading. The Tauri dev server watches Rust source files independently. However, if a build script re‑run causes the generated code to change, the dev server will pick up the subsequent Rust recompilation. In practice, adding rerun-if-changed for external dependencies makes the development loop seamless.

Conditional Compilation Based on Custom Features

Sometimes you want to compile different Tauri plugins or code paths depending on a Cargo feature flag. The build script can inspect environment variables that Cargo sets and emit cfg flags accordingly.

// src-tauri/build.rs
fn main() {
    // Example: enable an optional plugin only when a feature is active.
    #[cfg(feature = "database")]
    println!("cargo:rustc-cfg=tauri_plugin_sql");
    tauri_build::build()
}

The cargo:rustc-cfg instruction adds a custom cfg attribute that you can then use in src/lib.rs:

#[cfg(tauri_plugin_sql)]
tauri::Builder::default()
    .plugin(tauri_plugin_sql::Builder::new().build())
    // ...

This keeps the build script concise: it translates Cargo features into compile-time flags that the rest of the crate can act on.

Working with OUT_DIR for Your Own Generated Files

tauri_build::build() writes to OUT_DIR, but you can also write your own files there if you need extra code generation. The rule is strict: never write to src/ from a build script. Doing so will cause cargo publish to fail with an error like:

Source directory was modified by build.rs during cargo publish. Build scripts should not modify anything outside of OUT_DIR.

This restriction exists because published crate sources must remain immutable; only OUT_DIR is permitted. If you generate Rust code, place it in OUT_DIR and include it with include!:

// src-tauri/build.rs
use std::env;
use std::fs;
fn main() {
    let out_dir = env::var("OUT_DIR").unwrap();
    let dest_path = std::path::Path::new(&out_dir).join("my_generated.rs");
    fs::write(&dest_path, "pub const GENERATED: &str = \"hello\";").unwrap();
    tauri_build::build()
}
// src-tauri/src/lib.rs (inside your main crate code)
include!(concat!(env!("OUT_DIR"), "/my_generated.rs"));

Publish will break if you write to src/:

Any file creation or modification inside the source tree from a build script will cause cargo publish (and by extension tauri publish) to fail. Always use OUT_DIR for generated artifacts.

Common Misconceptions

“Build scripts control Tauri’s live reload”

The build script runs before Rust compilation. The tauri dev command uses its own file watcher to detect Rust source changes and trigger recompilation. The rerun-if-changed directives in build.rs only affect whether Cargo considers the build script itself outdated — they do not directly drive the hot‑reload loop. In Tauri v2, the dev server watches Cargo workspaces automatically, so you rarely need to manually add watch paths via the CLI unless you have very unusual project layouts.

“I need to add rerun-if-changed for every file in my project”

No. The default tauri_build::build() call already watches tauri.conf.json, the entire capabilities/ directory, and other Tauri‑specific inputs. Only add explicit directives for files or directories that Tauri does not already know about, such as a shared library outside the workspace or a non‑standard config file.

“If the build script fails, my frontend can still serve something”

If the build script fails, Cargo stops. The entire Rust compilation fails, and tauri dev will report an error. There is no graceful degradation — Tauri cannot start without the embedded configuration. This is intentional: a bad config should stop you early.

Best Practices

  • Keep the build script minimal. Delegate all Tauri‑specific logic to tauri_build::build(). Only add println! directives when you have a concrete need.
  • Use OUT_DIR exclusively for generated files. Never attempt to write into src/, capabilities/, or any other source directory.
  • Add rerun-if-changed for external dependencies. If your Cargo.toml references a path dependency outside the normal workspace root, tell Cargo about it in build.rs to avoid stale builds.
  • Do not run heavy computations in the build script. It executes on every relevant change, so keep it fast. Code generation, config parsing, and permission collection are already handled efficiently by tauri_build.
  • Test your build.rs changes with cargo check and a clean build. Run cargo clean followed by cargo build to verify that the script behaves correctly in a fresh environment.

Summary

The build.rs file is a standard Cargo build script that, in a Tauri project, invokes tauri_build::build() to embed your app’s configuration, permissions, and platform‑specific constants at compile time. Understanding its role helps you avoid confusion between Cargo’s incremental compilation and Tauri’s dev‑server hot‑reload, and it gives you the tools to safely extend the build process when your project calls for it.