Understanding src/main.rs in Tauri v2
Deep dive into the src/main.rs file — the desktop entry point of a Tauri v2 project. Learn what it does, why it exists, how it connects to lib.rs and Cargo.toml, and why you should rarely touch it.
Every Tauri v2 project has a file that looks almost too simple to be important: src/main.rs. A few lines of Rust, a single function call — and that is the whole file. Yet getting this file wrong can break both your desktop builds and the shared logic that powers your app on all platforms.
What src/main.rs Actually Is
src/main.rs is the binary entry point for the desktop version of your Tauri application. When you run cargo build or cargo run inside src-tauri, Cargo compiles this file into an executable binary. On mobile platforms, this file is completely ignored — the app is loaded as a library instead.
The entire purpose of src/main.rs is to hand control to the shared Rust logic that lives in src/lib.rs. It does not contain application code, command definitions, or plugin setup. It is a thin bridge between the operating system and the rest of your Tauri backend.
Why Tauri Splits Library and Binary
Tauri separates the Rust project into a library (lib.rs) and a desktop binary (main.rs) for one reason: mobile support.
On iOS and Android, the native framework loads your code as a shared library, not as a standalone executable. A binary entry point would be useless there. Instead, Tauri uses src/lib.rs as a single, unified entry point — the function run() — that both the desktop binary and the mobile platform loaders can call.
The desktop binary, defined in main.rs, is just a shell. It starts the process and immediately calls app_lib::run(). The mobile platform code calls that same run() function directly. All the real setup — plugins, commands, configuration — lives in lib.rs, so it works identically on every platform.
The Generated Code and What It Means
A freshly scaffolded Tauri v2 project produces this exact src/main.rs:
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
app_lib::run()
}
The first line is a crate-level attribute that applies only when the binary is compiled in release mode on Windows. It sets the Windows subsystem to "windows", which prevents a terminal console window from appearing alongside your app’s graphical window. Without it, users would see a blank command-line window pop up every time they launched your app — a jarring experience for a desktop GUI.
The main function is the standard entry point Rust uses for binaries. Inside it, a single line delegates to app_lib::run(). The name app_lib is not magic; it is the library crate name as defined in your Cargo.toml.
The Console Window on Windows:
The windows_subsystem attribute is easy to overlook. If you ever need to see Rust logs or print statements from your binary on Windows during debugging, keep the attribute as-is — it only removes the console in release builds. Debug builds automatically show the console so you can read output.
How app_lib::run() Connects to Cargo.toml
The app_lib in main.rs refers to the library target defined in src-tauri/Cargo.toml. If you open that file, you will see a [lib] section like this:
[package]
name = "my-tauri-app"
version = "0.1.0"
edition = "2021"
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
The name = "app_lib" line is what makes app_lib::run() work. If you ever change the library name to something else — for example, name = "core" — you must update main.rs to call core::run(). The Tauri scaffold ties these two together by default, and breaking that link will cause a compilation error.
The crate-type list tells Rust to produce several output formats from the library: a static library for mobile, a C dynamic library for certain embedders, and a standard Rust library for the desktop binary to link against. This is how one codebase can serve both binary and library consumers.
Renaming the Library Crate:
If you rename the library in Cargo.toml, update the call in main.rs and also check any imports inside lib.rs that might reference the crate by its original name. Forgetting this will produce cryptic “unresolved import” errors at compile time.
Why You Should Not Modify main.rs
Tauri’s official guidance is clear: do not add logic to main.rs. The file should stay as a one-line function call.
The reasoning goes beyond mere convention. Anything you place in main() runs before the Tauri application context exists. There is no AppHandle, no path resolver, no configuration loaded — none of the facilities your Rust commands rely on. If you try to create directories, read files, or initialize resources directly in main(), you will run into problems that are hard to debug:
- File paths relative to the working directory will point to unpredictable locations depending on how the user launched the app.
- OS-level permission errors, like
ReadOnlyFilesystemon macOS.appbundles or LinuxAppImage, will crash your application before the window even opens. - Mobile builds will ignore
main.rsentirely, so any startup logic placed there would silently never run on iOS or Android.
All initialization that requires Tauri’s APIs should live in lib.rs’s run() function, specifically inside the .setup() closure on the builder. That closure provides an AppHandle and lets you safely access path resolvers, the plugin system, and other Tauri context.
Startup Logic in main.rs Breaks Mobile:
Any code you add to main() will only execute on desktop. Mobile platforms skip the binary entry point entirely and load lib.rs as a library. If you rely on that startup code to set up databases, configurations, or state, your mobile app will silently miss that initialization — leading to crashes or empty data stores on Android and iOS.
What If You Really Need Custom Startup Behavior?
The correct place to add startup logic that runs on every platform — desktop and mobile — is the .setup() hook in src/lib.rs. Here is an example that creates a data directory safely using Tauri’s path resolver:
// src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
let data_dir = app.path().app_data_dir()?;
std::fs::create_dir_all(data_dir.join(".accounts"))?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
This code runs as soon as Tauri’s builder is configured but before the webview window opens. It works on desktop because the binary’s main() calls app_lib::run(), and it works on mobile because the platform loader calls the same run() function directly. No main.rs changes required.
If Your App Builds and Runs:
A strong indication that your main.rs is correct is that cargo tauri dev compiles without errors and your window appears. Since the file is so minimal, any mistake in it (wrong library name, missing crate attribute) will cause a build failure immediately — you will know something is wrong before the app even tries to start.
Common Misconceptions
“I can use main.rs for Rust-only utilities before the webview starts”
The instinct is understandable — main() seems like the perfect place to do file system setup or environment checks. But the Tauri runtime is not initialized yet. You have no controlled working directory, no permission scopes, and no integration with the frontend. Even simple std::fs calls can fail with permission errors depending on how the app was bundled and installed. The .setup() hook provides a guaranteed, cross-platform moment to perform that same work with full context.
“app_lib is a fixed name I must never touch”
app_lib is simply the default library name chosen by the scaffolder to avoid clashing with the binary’s crate name. It is technically arbitrary. You can rename it as long as you keep main.rs and Cargo.toml in sync. However, unless you have a strong reason to change it — like integrating into a larger Rust workspace with naming conventions — leaving it as app_lib keeps your project aligned with official examples and community patterns.
“The file is completely dead — I can delete it and use only lib.rs”
You could technically configure Cargo to treat the library as a binary with the right [[bin]] entries, but that would fight the scaffold’s intent. The split exists so that mobile builds never need to touch binary-only code, and the windows_subsystem attribute has a clean home that doesn’t pollute the library. Keeping main.rs as-is ensures your project works with the standard Tauri CLI commands and packaging pipeline without manual workarounds.
The Bigger Picture
src/main.rs embodies a design principle that runs through the entire Tauri project structure: separate what is platform-specific from what is shared. The file is 6 lines long because it only needs to express one thing — “start the library’s run function.” Everything else belongs in lib.rs, where it can be tested, reused, and kept identical across desktop and mobile.