The Tauri Documentation WIP

Tauri Banner

This is my work-in-progress version of the new tauri documentation. Beware of typos, inaccuracies, mistakes and missing pages!

Tauri is a toolkit that helps developers make applications for the major desktop platforms - using virtually any frontend framework in existence. The core is built with Rust, and the CLI leverages Node.js making Tauri a genuinely polyglot approach to creating and maintaining great apps.

Once in a while, we will show you code examples that do not work or do not compile. In most situations, we lead you to correct solution and the Rust mascot Ferris will always draw your attention to code that isn't meant to work:

FerrisMeaning
Ferris with a question markThis code does not compile!
Ferris throwing up their handsThis code panics!
Ferris with one claw up, shruggingThis code does not produce the desired behavior.

Prerequisites

The first step is to install Rust and System Dependencies. Keep in mind that this setup is only needed for developing Tauri apps. Your end-users are not required to do any of this. You'll need an internet connection for the download.

Setting Up Windows

For those using the Windows Subsystem for Linux (WSL), please refer to our Linux specific instructions instead.

On Windows, go to https://www.rust-lang.org/tools/install to install rustup the Rust installer. You also need to install Microsoft Visual Studio C++ build tools. The easiest way is to install Build Tools for Visual Studio 2019. When asked which workloads to install, ensure "C++ build tools" and the Windows 10 SDK are selected.

Install WebView2

WebView2 is pre-installed in Windows 11.

Tauri heavily depends on WebView2 to render web content on Windows, therefore you must have WebView2 installed. The easiest way is to download and run the Evergreen Bootstrapper from the official website.
The bootstrapper script will try to determine the correct architecture and version for your system. Still, if you run into issues - especially with Windows on ARM - you can select te correct Standalone Installer or even a fixed version.

Setting Up macOS

To install Rust on macOS, open a terminal and enter the following command:

curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

We have audited this bash script, and it does what it says it is supposed to do. Nevertheless, before blindly curl-bashing a script, it is always wise to look at it first. Here is the file as a plain download link.

The command downloads a script and starts the installation of the rustup tool, which installs the latest stable version of Rust. You might be prompted for your password. If the installation was successful, the following line will appear:

Rust is installed now. Great!

You also need to install CLang and macOS development dependencies. To do this, run the following command in your terminal:

xcode-select --install

Setting Up Linux

To install Rust on Linux, open a terminal and enter the following command:

curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

We have audited this bash script, and it does what it says it is supposed to do. Nevertheless, before blindly curl-bashing a script, it is always wise to look at it first. Here is the file as a plain download link.

The command downloads a script and starts the installation of the rustup tool, which installs the latest stable version of Rust. You might be prompted for your password. If the installation was successful, the following line will appear:

Rust is installed now. Great!

You also need to install a couple of system dependencies, such as a C compiler and webkit2gtk. Below are commands for a few popular distributions.

Debian

sudo apt update
sudo apt install libwebkit2gtk-4.0-dev \
    build-essential \
    curl \
    wget \
    libssl-dev \
    libgtk-3-dev \
    libappindicator3-dev \
    librsvg2-dev

Arch

sudo pacman -Syu
sudo pacman -S --needed \
    webkit2gtk \
    base-devel \
    curl \
    wget \
    openssl \
    appmenu-gtk-module \
    gtk3 \
    libappindicator-gtk3 \
    librsvg \
    libvips

Fedora

sudo dnf check-update
sudo dnf install webkit2gtk3-devel.x86_64 \
    openssl-devel \
    curl \
    wget \
    libappindicator-gtk3 \
    librsvg2-devel
sudo dnf group install "C Development Tools and Libraries"

Windows Subsystem for Linux (WSL)

To run a graphical application with WSL, you need to download one of these X servers: Xming, Cygwin X, and vcXsrv. Since vcXsrv has been used internally, it's the one we recommend installing.

WSL Version 1​

Open the X server and then run

export DISPLAY=:0

in the terminal. You should now be able to run any graphical application via the terminal.

WSL Version 2

You'll need to run a command that is slightly more complex than WSL 1:

export DISPLAY=$(cat /etc/resolv.conf | grep nameserver | awk '{print $2}'):0

and you need to add -ac to the X server as an argument. If for some reason this command doesn't work you can use an alternative command such as:

export DISPLAY=$(cat /etc/resolv.conf | grep nameserver | sed 's/.* //g'):0

or you can manually find the Address using:

cat /etc/resolve.conf | grep nameserver

Don't forget that you'll have to use the "export" command anytime you want to use a graphical application for each newly opened terminal.

You can download some examples to try with sudo apt-get install x11-apps. xeyes is always a good one. It can be handy when troubleshooting WSL issues.

Updating and Uninstalling

Tauri and its components can be manually updated by editing the Cargo.toml file or running the cargo upgrade command that is part of the cargo-edit tool. Open a terminal and enter the following command:

cargo upgrade

Updating Rust itself is easy via rustup. Open a terminal and run the following command:

rustup update

rustup can also be used to uninstall Rust from your machine fully:

rustup self uninstall

Troubleshooting

To check whether you have Rust installed correctly, open a shell and enter this line:

rustc --version

You should see the version number, commit hash, and commit date for the latest stable version that has been released in the following format:

rustc x.y.z (abcabcabc yyyy-mm-dd)

If you don't see this information, your Rust installation might be broken. Please consult Rust's Troubleshooting Section on how to fix this. If your problems persist, you can get help from the official Tauri Discord and GitHub Discussions.

Quick Start

Prerequisites

We expect you to have a working development environment from here on out. Please see the Prerequisites page if you haven't done this yet.

Don't know where to start?

HTML/CSS/JS

Prerequisites

We expect you to have a working development environment from here on out. Please see the Prerequisites page if you haven't done this yet.

The Tauri CLI

The Tauri CLI is the magic glue, that makes it all work: It orchestrates your frontend development server and cargo during development and bundles the rust binary and associated resources (sidecars or icons) into the final distributable app. You can install it from various sources, depending on your preference:

Cargo

The CLI is written in Rust, so its primary distribution mechanism is cargo:

cargo install tauri-cli

Please note that cargo has no support for prebuilt binaries, so the above command will always build the CLI from source.

JavaScript Package Managers

If you don't want to build the CLI from source or want to lock and version it for reproducible builds, we also distribute the CLI as an NPM package: @tauri-apps/cli.

npm

npm install --save-dev @tauri-apps/cli

yarn

yarn add -D @tauri-apps/cli

pnpm

pnpm add -D @tauri-apps/cli

Scaffold the Project

The easiest way to scaffold a Tauri app is create-tauri-app

npm

npx create-tauri-app

yarn

yarn create tauri-app

pnpm

pnpm create tauri-app

Just follow the prompts and select the Vanilla.js recipe!

Create the Frontend

Open a Window

Invoke Commands

Recap

Vite

Prerequisites

We expect you to have a working development environment from here on out. Please see the Prerequisites page if you haven't done this yet.

The Tauri CLI

The Tauri CLI is the magic glue, that makes it all work: It orchestrates your frontend development server and cargo during development and bundles the rust binary and associated resources (sidecars or icons) into the final distributable app. You can install it from various sources, depending on your preference:

Cargo

The CLI is written in Rust, so its primary distribution mechanism is cargo:

cargo install tauri-cli

Please note that cargo has no support for prebuilt binaries, so the above command will always build the CLI from source.

JavaScript Package Managers

If you don't want to build the CLI from source or want to lock and version it for reproducible builds, we also distribute the CLI as an NPM package: @tauri-apps/cli.

npm

npm install --save-dev @tauri-apps/cli

yarn

yarn add -D @tauri-apps/cli

pnpm

pnpm add -D @tauri-apps/cli

Scaffold the Project

The easiest way to scaffold a Tauri app is create-tauri-app

npm

npx create-tauri-app

yarn

yarn create tauri-app

pnpm

pnpm create tauri-app

Just follow the prompts and select the create-vite recipe!

Create the Frontend

Open a Window

Invoke Commands

Recap

Webpack

Prerequisites

We expect you to have a working development environment from here on out. Please see the Prerequisites page if you haven't done this yet.

The Tauri CLI

The Tauri CLI is the magic glue, that makes it all work: It orchestrates your frontend development server and cargo during development and bundles the rust binary and associated resources (sidecars or icons) into the final distributable app. You can install it from various sources, depending on your preference:

Cargo

The CLI is written in Rust, so its primary distribution mechanism is cargo:

cargo install tauri-cli

Please note that cargo has no support for prebuilt binaries, so the above command will always build the CLI from source.

JavaScript Package Managers

If you don't want to build the CLI from source or want to lock and version it for reproducible builds, we also distribute the CLI as an NPM package: @tauri-apps/cli.

npm

npm install --save-dev @tauri-apps/cli

yarn

yarn add -D @tauri-apps/cli

pnpm

pnpm add -D @tauri-apps/cli

Scaffold the Project

The easiest way to scaffold a Tauri app is create-tauri-app

npm

npx create-tauri-app

yarn

yarn create tauri-app

pnpm

pnpm create tauri-app

While we offer no "plain webpack" recipe, the Vue CLI recipe uses webpack under the hood.

Create the Frontend

Open a Window

Invoke Commands

Recap

Background

Now that you have completed the Quick Start and have a basic Tauri application at hand, it is tempting to jump right in. I invite you, however, to resist this temptation for a couple more pages and learn more about the concepts and ideas behind Tauri. You will find developing an app is much like creating a traditional client-server application on the web, with a couple of subtle but important differences.

This chapter will cover Tauris multi-process architecture, windows, and webviews and our design decisions to make your application more secure and resource efficient.

Process Model

Tauri employs a multi-process architecture similar to Electron or many modern web browsers. This guide explores the reasons behind the design choice and why it is key to writing secure applications.

Why Multiple Processes?

In the early days of GUI applications, it was common to use a single process to perform computation, draw the interface and react to user input. As you can probably guess, this meant that a long-running, expensive computation would leave the user interface unresponsive or, worse, a failure in one app component would bring the whole app crashing down.

It became clear that a more resilient architecture was needed, and applications began running different components in different processes. This makes much better use of modern multi-core CPUs and creates far safer applications. A crash in one component doesn't affect the whole system anymore, as components are isolated on different processes. If a process gets into an invalid state, we can easily restart it.

We can also limit the blast radius of potential exploits by handing out only the minimum amount of permissions to each process, just enough so they can get their job done. This pattern is known as the Principle of least privilege, and you see it in the real world all the time. If you have a gardener coming over to trim your hedge, you give them the key to your garden. You would not give them the keys to your house; why would they need access to that? The same concept applies to computer programs. The less access we give them, the less harm they can do if they get compromised.

The Core Process

Each Tauri application has a single core process, which acts as the application's entry point and which is the only component with full access to the operating system.

The Core's primary responsibility is to use that access to create and orchestrate application windows, system-tray menus, or notifications. Tauri implements the necessary cross-platform abstractions to make this easy. It also routes all Inter-Process Communication through the Core process, allowing you to intercept, filter, and manipulate IPC messages in one central place.

The Core process should also be responsible for managing global state, such as settings or database connections. This allows you to easily synchronize state between windows and protect your business-sensitive data from prying eyes in the Frontend.

We chose Rust to implement Tauri because its concept of Ownership guarantees memory safety while retaining excellent performance.

flowchart TD
    C{Core}
    W1[WebView]
    W2[WebView]
    W3[WebView]

    C <-->|Events & Commands| W1
    C <-->|Events & Commands| W2
    C <-->|Events & Commands| W3
Figure 1-1: Simplified representation of the Tauri process model. A single Core process manages one or more WebView processes.

The WebView Process

The Core process doesn't render the actual user interface (UI) itself; it spins up WebView processes that leverage WebView libraries provided by the operating system. A WebView is a browser-like environment that executes your HTML, CSS, and JavaScript.

This means that most of your techniques and tools used in traditional web development can be used to create Tauri applications. For example, many Tauri examples are written using the Svelte frontend framework and the Vite bundler.
Security best practices apply as well; for example, you must always sanitize user input, never handle secrets in the Frontend and ideally defer as much business logic to the Core process as possible to keep your attack surface small.

Contrary to other similar solutions, the WebView libraries are not included in your final executable but dynamically linked at runtime1. This makes your application significantly smaller, but it also means that you need to keep platform differences in mind, just like traditional web development.

1

Currently, Tauri uses Microsoft Edge WebView2 on Windows, WKWebView on macOS and webkitgtk on Linux.

Inter-Process Communication

Inter-Process Communication (IPC) allows isolated processes to communicate securely and is key to building more complex applications.

Tauri uses a particular style of Inter-Process Communication called Asynchronous Message Passing, where processes exchange requests and responses serialized using some simple data representation. Message Passing should sound familiar to anyone with web development experience, as this paradigm is used for client-server communication on the internet.

Message passing is a safer technique than shared memory or direct function access because the recipient is free to reject or discard requests as it sees fit. For example, if the Tauri Core process determines a request to be malicious, it simply discards the requests and never executes the corresponding function.

In the following, we explain Tauri's two IPC primitives - Events and Commands - in more detail.

Events

Events are fire-and-forget, one-way IPC messages that are best suited to communicate lifecycle events and state changes. Contrary to Commands Events can be emitted by both the Frontend and the Tauri Core.

sequenceDiagram
    participant F as Frontend
    participant C as Tauri Core

    C-)F: Event
Figure 1-2: An event sent from the Core to the Frontend.

Commands

Tauri also provides a foreign function interface-like abstraction on top IPC messages1. The primary API, invoke, is similar to the browsers fetch API and allows the Frontend to invoke rust functions, pass arguments, and receive data.

Because this mechanism uses the JSON-RPC protocol under the hood to serialize requests and responses, all arguments and return data must be serializable to JSON.

sequenceDiagram
    participant F as Frontend
    participant C as Tauri Core

    F-)+C: IPC request
    note over C: Perform computation, write to file system, etc.
    C-)-F: Response
Figure 1-3: IPC messages involved in a command invocation.
1

Commands still use message passing under the hood, so don't share the same security pitfalls as real FFI interfaces.

Security

Whether you like it or not, today's applications live in operating systems that can be -- and regularly are -- compromised by any number of attacks. When your insecure application is a gateway for such lateral movement into the operating system, you are contributing to the tools that professional hackers have at their disposal. Don't be a tool.

This is why we have taken every opportunity to help you secure your application, prevent undesired access to system level interfaces, and manufacture bullet-proof applications. Your users assume you are following best practices. We make that easy, but you should still read up on it below.

Security Is A Community Responsibility

It is important to remember that the security of your Tauri application is the result of the overall security of Tauri itself, all Rust and NPM dependencies, your code, and the devices that run the final application. The Tauri Team does its best to do its part, the security community does its part, and you too would do well to follow a few important best practices:

  • Keep your application up-to-date. When releasing your app into the wild, you are also shipping a bundle that has Tauri in it. Vulnerabilities affecting Tauri may impact the security of your application. By updating Tauri to the latest version, you ensure that critical vulnerabilities are already patched and cannot be exploited in your application. Also be sure to keep your compiler (rustc) and transpilers (nodejs) up to date, because there are often security issues that are resolved.

  • Evaluate your dependencies. While NPM and Crates.io provide many convenient packages, it is your responsibility to choose trustworthy 3rd-party libraries - or rewrite them in Rust. If you do use outdated libraries affected by known vulnerabilities or are unmaintained, your application security and good-night's sleep could be in jeopardy. Use tooling like npm audit and cargo audit to automate this process and lean on the security community's important work.

  • Adopt more secure coding practices. The first line of defense for your application is your own code. Although Tauri can protect you from common web vulnerabilities, such as Cross-Site Scripting based Remote Code Execution, improper configurations can have a security impact. Even if this were not the case, it is highly recommended to adopt secure software development best practices and perform security testing. We detail what this means in the next section.

  • Educate your Users. True security really means that unexpected behaviour cannot happen. So in a sense, being more secure means having the peace of mind in knowing that ONLY those things that you want to happen can happen. In the real world, though, this is a utopian "dream". However, by removing as many vectors as possible and building on a solid foundation, your choice for Tauri is a signal to your users that you really care about them, their safety, and their devices.

Threat Models

Tauri applications are composed of many pieces at different points of the lifecycle. Here we describe classical threats and what you SHOULD do about them.

  • Upstream Threats. Tauri is a direct dependency of your project, and we maintain strict authorial control of commits, reviews, pull requests, and releases. We do our best to maintain up-to-date dependencies and take action to either update or fork&fix. Other projects may not be so well maintained, and may not even have ever been audited. Please consider their health when integrating them, because otherwise you may have adopted architectural debt without even knowing it.

  • Development Threats. We assume that you, the developer, care for your development environment like a shrine of purity because it is a thing of beauty. It is on you to make sure that your operating system, build toolchains, and associated dependencies are kept up to date.

    A genuine risk all of us face is what is known as "supply-chain attacks", which are usually considered to be attacks on direct dependencies of your project. However, a growing class of attacks in the wild directly target development machines, and you would be well-off to address this head-on.
    One practice that we highly recommend, is to only ever consume critical dependencies from git using hash revisions at best or named tags as second best. This holds for Rust as well as the Node ecosystem. Also, consider requiring all contributors to sign their commits and protect GIT branches and pipelines.

  • Buildtime Threats. Modern organisations use CI/CD to manufacture binary artifacts. At Tauri, we even provide a Github Workflow for building on multiple platforms. If you create your own CI/CD and depend on third-party tooling, be wary of actions whose versions you have not explicitly pinned.
    You should sign your binaries for the platform you are shipping to, and while this can be complicated and somewhat costly to setup, end-users expect that your app is verifiably from you.

  • Runtime Threats. We assume the webview is insecure, which has led Tauri to implement several protections regarding webview access to system APIs in the context of loading untrusted userland content.
    You can read more in detail below, but using the CSP will lockdown types of communication that the Webview can undertake. Furthermore, Context Isolation prevents untrusted content or scripts from accessing the API within the Webview.
    And please, whatever you do, DO NOT trust the results of cryptography using private keys in the Webview. We gave you Rust for a reason.

  • Updater Threats. We have done our best to make shipping hot-updates to the app as straightforward and secure as possible. However, all bets are off if you lose control of the manifest server, the build server, or the binary hosting service. If you build your own system, consult a professional OPS architect and build it properly.

Secure content loading

Tauri restricts the Content Security Policy (CSP) of your HTML pages. Local scripts are hashed, styles and external scripts are referenced using a cryptographic nonce, which prevents unallowed content from being loaded.

❗️ Avoid loading remote content such as scripts served over a CDN as they introduce an attack vector, but any untrusted file can introduce new and subtle attack vectors.

The CSP protection is only enabled if [tauri > security > csp] is set on the Tauri configuration file. You should make it as restricted as possible, only allowing the webview to load assets from hosts you trust and preferably own. At compile time, Tauri appends its nonces and hashes to the relevant CSP attributes automatically, so you only need to worry about what is unique to your application.

See script-src, style-src and CSP Sources for more information about this protection.

Context Isolation

Context Isolation is a way to intercept and modify Tauri API messages sent by the Frontend before they get to Tauri Core, all with JavaScript. The secure JavaScript code that is injected by the Isolation pattern is referred to as the Isolation application.

This is useful to validate, sanitize, and filter messages sent to the front end before they even enter the Core's secure context.

Why?​

The Isolation pattern's purpose is to provide a mechanism for developers to help protect their application from unwanted or malicious frontend calls to Tauri Core. The need for the Isolation pattern rose out of threats coming from untrusted content running on the Frontend, a common case for applications with many dependencies. See Security: Threat Models for a list of many sources of threats that an application may see.

The largest threat model described above that the Isolation pattern was designed in mind with was Development Threats. Not only do many frontend build-time tools consist of many dozen (or hundreds) of often deeply-nested dependencies, but a complex application may also have a large amount of (also often deeply-nested) dependencies that are bundled into the final output.

When?

Tauri highly recommends using the isolation patten whenever it can be used. Because the Isolation application intercepts all messages from the Frontend, it can always be used.

We highly suggest that you lock down your application whenever you use external Tauri APIs. As the developer, you can utilize the secure Isolation application to verify IPC inputs make sure they are within some expected parameters. For example, you may want to check that a call to read or write a file is not trying to get to a path outside your application's expected locations.
Another example is making sure that a Tauri API HTTP fetch call is only setting the Origin header to what your application expects it to be.

That said, it intercepts all messages from the Frontend, so it will even work with always-on APIs such as Events. Since some events may cause your rust code to perform actions, the same validation techniques can be used.

How?

An Isolation Application is essentially a just JavaScript file that Tauri will run in a locked-down, isolated environment. You assign a callback to the window.__TAURI_ISOLATION_HOOK__ global property that Tauri will invoke whenever an IPC message is about to be sent.

Because the point of the Isolation application is to protect against Development Threats, we highly recommend keeping your Isolation application as simple as possible.
Fewer dependencies and build steps mean less risk of supply chain attacks against your Isolation application.

Creating the Isolation Application

We will make a small hello-world style Isolation application and hook it up to an imaginary existing Tauri application. It will do no verification of the messages passing through it, only print the contents to the WebView console.

For the purposes of this example, let's imagine we are in the same directory as tauri.conf.json. The existing Tauri application has it's distDir set to ../dist.

Filename: index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Isolation Secure Script</title>
  </head>
  <body>
    <script src="index.js"></script>
  </body>
</html>
Listing 1-1: HTML entrypoint responsible for loading the JavaScript.

Filename: index.js

window.__TAURI_ISOLATION_HOOK__ = (payload) => {
  // let's just print payload
  console.log("hook", payload);
  return payload;
};
Listing 1-2: The main Isolation Application script.

We need to set up our tauri.conf.json configuration to enable Context Isolation.

Configuration

Let's assume that our main frontend distDir is set to ../dist. We also output the previously created Isolation application to ../dist-isolation.

Filename: tauri.conf.json

{
  "build": {
    "distDir": "../dist"
  },
  "tauri": {
    "pattern": {
      "use": "isolation",
      "options": {
        "dir": "../dist-isolation"
      }
    }
  }
}

Digging Deeper

If you want to fully understand how the Context Isolation is implemented, so you can properly secure your application, we go into more detail below.

sequenceDiagram
    autonumber

    participant F as Frontend
    participant H as Isolation Handler
    participant I as Isolation Application
    participant C as Tauri Core

    F-)H: Send Message
    H-)+I: Call the Isolation Application with Message
    I-)-H: Return sanitized Message
    note over H: Automatically encrypt sanitized Message
    H-->>F: Send encrypted, sanitized Message
    F-->>C: Send encrypted, sanitized Message
Figure 1-4: Approximate Steps of an IPC Message​ being sent to Tauri Core with Context Isolation enabled.
  1. When invoke is called, the Message gets sent to the Isolation Application.
  2. Pass the message into the Isolation Application's hook. The Message object has been checked to follow the minimum correct shape.
  3. The return value is used as the new Message. The Isolation Application may have modified Message to help sanitize input.
  4. The sanitized Message is automatically encrypted using AES-GCM using a runtime-generated key and sent to the Frontend.
  5. The encrypted, sanitized message is sent to Tauri Core; which exclusively processes encrypted messages while Context Isolation is enabled.

Performance Implications

Because encryption of the message does occur, this does mean that there are additional overhead costs even if the secure Isolation application doesn't do anything. Most applications should not notice the runtime costs of encrypting/decrypting the IPC messages as they are both relatively small and AES-GCM is relatively fast. If you are unfamiliar with AES-GCM, all that is relative in this context is that it's the only authenticated mode algorithm included in SubtleCrypto and that you probably already use it every day under the hood with TLS.

There is also a cryptographically secure key generated once each time the Tauri application is started. It is not generally noticeable if the system already has enough entropy to immediately return enough random numbers, which is extremely common for desktop environments. If running in a headless environment to perform some [end-to-emd testing with WebDriver] then you may want to install some sort of entropy generating service such as haveged if your operating system does not have one included.

Linux 5.6 (March 2020) now includes entropy generation using speculative execution.

Limitations

There are a few limitations with Context Isolation due to platform inconsistencies. The most significant limitation is due to external files not loading correctly inside sandboxed iframes on Windows. Because of this, we have implemented a simple script inlining step during build time that takes the content of scripts relative to the Isolation application and injects them inline. This means that typical bundling or simple including of files like <script src="index.js"></script> still works properly, but newer mechanisms such as ES Modules will not successfully load.

Inter-Process Communication

We established the general concepts and ideas behind IPC in Background: Inter-Process Communication. This guide teaches you how to put that theory to practice and interact with the IPC system from both JavaScript and Rust.

Events

Events are one-way IPC messages and come in two distinct flavors: Global Events and Window-specific Events. Global events are emitted for application-wide lifecycle events (e.g. update events), while window-specific events are, as the name suggests, emitted for window lifecycle events like resizing, moving, or user-defined events.

Commands

At its simplest, a Command is a Rust function that is invoked in response to an incoming IPC request. This function has access to the application state, windows, may take input parameters, and returns data. You can think of them almost like [Serverless Functions] that live in the Tauri Core process and communicate over IPC.

To turn a Rust function into a Command, add #[tauri::command] to the line before fn. This Attribute Macro wraps your function, handles JSON serialization, and injects Special Paramaters.

#[tauri::command]
fn my_custom_command() {
  println!("Hello, world!");
}

Listing 2-TODO: A regular Rust function turned into a Command by the tauri::command macro.

You can use the invoke() function provided by the @tauri-apps/api package to call Commands from the Frontend. The function requires the Command name and optional parameters and returns a promise that resolves when the Command finished executing:

import { invoke } from "@tauri-apps/api";

await invoke("my_custom_command");
Listing 2-TODO: A command invocation without parameters.

Parameters

Commands can have parameters, which are defined like regular Rust function parameters. Tauri will reject IPC requests for a command if the argument number, types, or names are invalid.

All parameters must implement serde::Deserialize so the tauri::command macro can correctly parse the incoming IPC request. Standard types such as u8, String or bool are deserializable by default, but you have to derive or manually implement serde::Deserialize for types you defined yourself.

#[tauri::command]
fn my_command(msg: String) {
    println!("I was invoked with this message: {}", msg);
}
Listing 2-TODO: Simple command accepting only a single parameter.

Special Parameters

Commands that only have access to the parameters passed from the Frontend aren't too helpful, so the tauri::command macro has a couple of tricks up its sleeve. If you specify any of the following types as parameters to your function, they will be automagically injected by the macro.

The tauri::command macro strips Special Parameters from the function signature, so they are invisible to the Frontend as Listing 2-TODO shows.

#[tauri::command]
fn my_command(window: tauri::Window, _app_handle: tauri::AppHandle) {
    println!("I was invoked from window: {}", window.label());
}
invoke("my_command");
Listing 2-TODO: Special Parameters are invisible to the Frontend.

You can instruct the macro to inject globally managed state by using the tauri::State type. This only works with types that you previously stored in the globaly state with manage(), see the State Management guide for more details.

struct DBConnection(Option<DBClient>);

#[tauri::command]
fn is_connected(connection: State<'_, DBConnection>) -> bool {
    // return true if `connection` holds a `DBClient`
    connection.0.is_some()
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![is_connected])
        .setup(|app| {
            app.manage(DBConnection(None));

            Ok(())
        })
        .run()
        .expect("failed to run app");
}

Listing 2-TODO: Using State to inject global application state in commands.

Note: It's an informal convention that you put Special Parameters before any regular Parameters.

Commands with Return Values

Commands can return values to the Frontend, exactly like regular Rust functions, with one caveat: Return values must be representable as JSON. In Rust we say that the type needs to implement serde::Serialize.

Most standard types such as u8, String or bool already implement serde::Serialize by default and even more complex types such as HashMap<K,V> can be serialized as long as both generic types implement serde::Serialize. For types that you defined yourself you need to either derive the trait or implement it manually.

#[tauri::command]
fn simple_command() -> String {
  "Hello from Rust!".into()
}

#[derive(serde::Serialize)]
struct Data {
  some_key: String,
  some_other_key: bool,
  more_complex_value: HashMap<String, DataInner>
}

#[derive(serde::Serialize)]
struct DataInner(Vec<u8>);

#[tauri::command]
fn complex_command() -> Data {
  let mut map = HashMap::default();

  map.insert("first", DataInner(vec![1,2,3,4]));

  Data {
    some_key: "foobar".to_string(),
    some_other_key: true,
    more_complex_value: map,
  }
}
const msg = invoke("simple_command");

// prints "Hello from Rust!"
console.log(msg.payload);

const data = invoke("complex_command");

// prints the following:
// {
//  some_key: "foobar",
//  some_other_key: true,
//  more_complex_value: {
//      first: [1,2,3,4]
//  }
// }
console.log(data.payload);

Listing 2-TODO: Simple and complex command return values, showing how derive(Serde::Serialize) can be used to return user-defined types.

Error handling

Rust has a standard way to represent failures in functions: The Result<T, E> type. It is an enum with two variants, Ok(T), representing success, and Err(E), representing error.

As you learned earlier Command invocations are represented by a JavaScript promise. By returning a Result from your Command you can directly influence the state of that promise: Returning Ok(T) resolves the promise with the given T, while returning Err rejects the promise with E as the error.

#[tauri::command]
fn failing_command() -> Result<String, String> {
  Err("oops!".to_string())
}
Listing 2-TODO: A Command that always fails.

If you try this using real-world functions, however, you quickly run into a problem: No error type implements serde::Serialize!

use std::fs::File;
use std::io;
use std::io::Read;

#[tauri::command]
fn read_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;

    Ok(s)
}

Listing 2-TODO: This code does not compile because std::io::Error is not serializable.

You could just use an error type, for example, String like we did in Listing-TODO, but that is not very idiomatic. Instead, we create a custom error type that implements serde::Serialize.
In the following example, we use a crate called thiserror to help create the error type. It allows you to turn enums into error types by deriving the thiserror::Error trait. You can consult its documentation for more details.

// create the error type that represents all errors possible in our program
#[derive(Debug, thiserror::Error)]
enum Error {
  #[error(transparent)]
  Io(#[from] std::io::Error)
}

// we must manually implement serde::Serialize
impl serde::Serialize for Error {
  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
  where
    S: serde::ser::Serializer,
  {
    serializer.serialize_str(self.to_string().as_ref())
  }
}

A custom error type has the advantage of making all possible errors explicit so readers can quickly identify what errors can happen. This saves other people (and yourself) enormous amounts of time when reviewing and refactoring code later.
It also gives you full control over the way your error type gets serialized. In the above example, we simply returned the error message as a string, but you could assign each error a code similar to C.

Async Commands

If your Command spends time waiting for IO - maybe it is reading a file or connecting to a server - it blocks the main process for that duration. This means the window becomes unresponsive, and your app freezes. To avoid this problem Rust has builtin support asynchronous functions through the Future Trait. A familiar concept if you already know about Promise and async/await in JavaScript.

You declare an asynchronous command by writing async fn instead of fn:

#[tauri::command]
async fn async_command() {}

Async Commands are executed on a thread pool using tauri::async_runtime::spawn(), so long-running tasks no longer block the Core's main thread. Because Commands map to JavaScript promises in the Frontend, they also don't block the Frontend's main thread.

To execute non-async, regular Commands on a different thread, define the macro like so: #[tauri::command(async)].

Listing-TODO shows a more complete example that uses the non-blocking tokio::fs::read() function to read a file from disk, convert it to a Utf8 string and parse it into a Vec of lines. It also uses the previously introduced thiserror and serde::Serialize to create a custom Error type.

use std::path::PathBuf;

// A custom error type that represents all possible in our command
#[derive(Debug, thiserror::Error)]
enum Error {
    #[error("Failed to read file: {0}")]
    Io(#[from] std::io::Error),
    #[error("File is not valid utf8: {0}")]
    Utf8(#[from] std::string::FromUtf8Error),
}

// we must also implement serde::Serialize
impl serde::Serialize for Error {
  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
  where
    S: serde::ser::Serializer,
  {
    serializer.serialize_str(self.to_string().as_ref())
  }
}

async fn async_read_lines(path: PathBuf) -> Result<Vec<String>, Error> {
    // read bytes a non-blocking way
    let bytes = tokio::fs::read(path).await?;

    // convert bytes into utf8 string
    let string = String::from_utf8(bytes)?;

    // splitting at newline characters
    let lines = string
        .split('\n')
        .map(|line| line.to_string())
        .collect::<Vec<_>>();

    Ok(lines)
}
Listing 2-TODO: A more complete examples that uses non-blocking APIs to read a file into a Vec of lines.

Windows & Webviews

Windows

A Tauri application consists of one or more windows that are managed by the Core process. Each window is identified by a unique string label that you can freely choose when creating the window. You can use this label to retrieve a reference to a specific window later to, for example, resize a specific window.

The WindowBuilder can be used to configure and create windows with a wide range of configurable options, as you can see in the Listing-TODO below.

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            WindowBuilder::new(
                    app,
                    "example_window", // the unique label
                    WindowUrl::App("index.html".into())
                )
                .title("Example Window")
                .resizable(true)
                .min_inner_size(1000,500)
                .max_inner_size(1200,700)
                .always_on_top(true)
                .build()
                .expect("failed to create example window");
        })
        .run()
        .expect("failed to run app");
}

Listing 2-TODO: Creating a new using the WindowBuilder and setting various options.

If you are unfamiliar with Rust programming or are looking for a quick, no-hassle way to create windows, Tauri also supports declaring them in the tauri.conf.json file.

Filename: tauri.conf.json

//...
"tauri": {
    "windows": [
        {
            "title": "Welcome to Tauri!",
            "width": 800,
            "height": 600,
            "resizable": true,
            "fullscreen": false
        }
    ],
}
//...

The WebView

Each window contains one webview that lets you render the actual UI using HTML, CSS and JavaScript. This makes Tauri compatible with virtually any frontend framework in existence.

During development you point Tauri at a localhost URL - your development server - so that you can leverage hot module reloading (HMR) provided by your favourite frontend build tool.
For production builds however, you need to hand over static files that Tauri will inline into the final binary during building. This should feel familiar if you have build a website using a static file hosting service like Netlify or GitHub Pages before.

In the following examples we will be using the Vite frontend bundler, but you can choose any Frontend build tools that can produce static files.

Filename: index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0"
    />
  </head>
  <body>
    <h1>Hello World</h1>
  </body>
</html>

File tauri.conf.json

"build": {
    // the command that will start our local development server
    "beforeDevCommand": "vite",
    // the localhost URL that our development server is listening on
    "devPath": "http://localhost:3000",
    // the command that will produce the static files during building
    "beforeBuildCommand": "vite build",
    // the directory where the static files will be placed by vite
    "distDir": "dist"
}

As you learned previously, Tauri does ship a webview but relies on the webviews provided by each operating system instead. This means that not all browser APIs will be supported on all platforms you target, WebView2 uses an evergreen updater that always gives you the latest Chromium Features, while WKWebview versions are tied to the macOS versions. You can refer to Appendix C: Version Tables to get a detailed list of macOS versions and corresponding safari and webkit versions.

Don't despair however, platform differences are common in web development (think of IE11) and there are many tools that can aid you in writing elegant cross-platform JavaScript.

Note: ES2021 is supported across all Tauri platforms, so most language features should work out-of-the-box without transpilation.

  1. Use a Transpiler. Transpilers like [Babel] take your modern JavaScript and produce Code that works on older platforms, polyfilling unsupported features in the process. If you're using Typescript you already have a builtin transpiler too!

  2. Use feature detection. Feature detection is a good practice on the web in general, but with Tauri you can make use of several build-time environment variables like TAURI_PLATFORM or TAURI_PLATFORM_VERSION to generate platform-specific JavaScript. These variables are exposed to the beforeDevCommand and beforeBuildCommand by default.

    Filename: vite.config.js
     {
         // to make use of
         // `TAURI_PLATFORM`, `TAURI_ARCH`, `TAURI_FAMILY`,
         // `TAURI_PLATFORM_VERSION`, `TAURI_PLATFORM_TYPE`
         // and `TAURI_DEBUG` env variables in the Frontend
         envPrefix: ["VITE_", "TAURI_"],
         build: {
             // tauri supports es2021
             target: [
             "es2021",
             process.env.TAURI_PLATFORM === "windows" ? "chrome97" : "safari13",
             ],
             // don't minify for debug builds
             minify: !process.env.TAURI_DEBUG && "esbuild",
             // produce sourcemaps for debug builds
             sourcemap: !!process.env.TAURI_DEBUG,
         },
     };
    

    Listing 2-TODO: Conditional compilation in vite using TAURI_ environment variables.

  3. Use Rust. Instead of relying on features that are not supported across all platforms you can replace them with Rust implementations that are exposed via Commands. The tauri-plugin-store is an example of such a practice, it replaces LocalStorage with a much more customizable solution written in Rust.

Debugging

With all the moving pieces in a Tauri application, chances are you will not write perfect bug-free code all the time. Your app might behave weirdly, be very slow, or outright crash.

In this guide, we give you a number of tools and techniques to troubleshoot problems when they arise.

Rust

To effectively debug a program, you need to know what's going on inside. Rust and Tauri provide many tools to make this possible.

Logging

When your first learned Rust, you might have printed logging messages by using the println! macro:

fn main() {
    println!("foobar");
}

However, for more complex projects, Rust provides an elegant logging system that allows log messages from app code and dependencies with different levels, timestamps, and metadata.

To use this system, add the log crate to your Cargo.toml file:

[package]
name = "app"
version = "0.1.0"
edition = "2021"

[dependencies]
log = "0.4"

Now you can use a number of logging macros: error!, warn!, info!, debug! and trace! where error! represents the highest-priority log.

use log::{trace, debug, info, warn, error};

fn main() {
    trace!("A trace-level message");
    debug!("A debug-level message");
    info!("An info-level message");
    warn!("A warn-level message");
    error!("An error-level message");
}

However, you will notice it doesn't actually print anything when you run this! This is because log expects you to bring your own logger. There are many available implementations to choose from, here are some of the most popular ones:

tauri-plugin-log

The Tauri team maintains a logger that is built explicitly for Tauri applications. It is built on top of fern and supports writing logs to many different targets and consuming log messages produced in the WebView.

Add it to your Cargo.toml:

[package]
name = "app"
version = "0.1.0"
edition = "2021"

[dependencies]
log = "0.4"
tauri-plugin-log = { git = "https://github.com/tauri-apps/tauri-plugin-log" }

and import it like any other Tauri plugin:

use tauri_plugin_log::{LogTarget, LoggerBuilder};

fn main() {
    tauri::Builder::default()
        .plugin(
            LoggerBuilder::new()
                .targets([
                    // write to the OS logs folder
                    LogTarget::LogDir,
                    // write to stdout
                    LogTarget::Stdout,
                    // forward logs to the webview
                    LogTarget::Webview,
                ])
                .build(),
        )
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
Listing 2-TODO: Example configuration that emits logs to the WebView, Stdout and to the OS's log folder.

Tracing

Sometimes simple logs are not enough to debug your problem, though, so you might reach for the tracing crate.

In addition to logging-style diagnostics recorded by the log crate, it provides information about temporality and causality. Spans in tracing are events that have a beginning and end time, may be entered and exited by the flow of execution, and may exist within a nested tree of similar spans.


#![allow(unused)]
fn main() {
use tracing::{info, debug, span, Level};

// records an event outside of any span context:
info!("something happened");

let span = span!(Level::INFO, "my_span");
let _guard = span.enter();

// records an event within "my_span".
debug!("something happened inside my_span");
}

GDB

The GNU Project Debugger (GDB) is a very old program written by Richard Stallman in 1986. GDB has support for several languages, such as C/C++, but also modern languages such as Go and Rust.

rust-gdb comes with the Rust installation by default and is a wrapper around GDB that enables pretty-printing rust types in the GDB output.

To debug a Rust binary, build the binary:

cargo build --debug

And then load it into GDB:

rust-gdb target/debug/<app name>

LLDB

LLDB is a debugger built on top of LLVM, the compiler backend used by Rust itself. We can use the rust-lldb tool, which comes with the Rust installation by default. It wraps LLDB to provide pretty-printing rust types.

To debug a Rust binary, build the binary:

cargo build --debug

And then load it into LLDB:

rust-lldb target/debug/<app name>

Panics

When your Rust code runs into a problem that is so severe that it can't recover from it, your program should panic. When a Panic occurs, your program will print a failure message, unwind and clean up the stack, and then quit. A Panic will manifest as a hard crash, so it's crucial to minimize the number of panics.

To determine what caused the Panic, you can re-run your application with the RUST_BACKTRACE environment variable set to 1 to print a more detailed failure message.

JavaScript

To open the WebView dev tools, right-click in the WebView and choose Inspect Element. This opens up the web-inspector similar to the one you're used to from Chrome, Firefox, or Safari.

If you run into problems with your frontend framework, you might reach for framework-specific dev tools. While many of them are distributed as Chromium Extensions, which are not compatible with Tauri, some of them - such as the Vue Devtools - provide standalone versions that work nicely with Tauri.

tauri-plugin-log-api

As an alternative to the ubiquitous console.log debugging, tauri-plugin-log offers a JavaScript API that has a very similar feature set to the Rust version.
You can install it from npm with the following command:

npm

npm install --save-dev tauri-plugin-log-api

yarn

yarn add -D tauri-plugin-log-api

pnpm

pnpm add -D tauri-plugin-log-api

Now you can emit logs using the trace(), debug(), info(), warn() and error() functions and attach the devtools console to the loggers event stream by calling attachConsole():

import {
  attachConsole,
  trace,
  debug,
  info,
  warn,
  error,
} from "tauri-plugin-log-api";

// with LogTarget::Webview enabled this function will print logs to the browser console
const detach = await attachConsole();

trace("A trace-level message");
debug("A debug-level message");
info("An info-level message");
warn("A warn-level message");
error("An error-level message");

// detach the webview console from the log stream
detach();

Please refer to the plugin's documentation for details.

Debugging Production Builds

Not all bugs are found during development; some will be reported by your end-users. Below are some tips to help you debug production builds.

Tauri plugin log

tauri-plugin-log can be used in production code too. When configured with the LogTarget::LogDir it will write logs to the canonical log-file directory of your Operating System. When your application crashes, you can recover logs from those locations, e.g., the Console application can be used to view log files on macOS:

Apple Console App
Listing 2-TODO: Apple's Console App showing showing the Tauri app's log file.

Testing

Automated tests are essential to ensure your application's stability, quality, and correctness. It's common to write Tests for libraries or server-side code, but many people struggle with writing automated tests for Graphical User Interface (GUI) applications.
Following the separation of concerns you learned about in the Process Model chapter, we recommend you set up two kinds of tests:

  • Unit Tests - Test frontend and Core functionality independently in isolated contexts.
  • End-to-End Tests (E2E) - Spiin up full instances of your app and simulate real user interactions to make sure individually tested components work well together.

In this guide we walk you through setting up units tests for Rust and JavaScript, as well as End-to-End tests using WebDriver.

Unit Testing

Unit tests verify that individual units of source code are functioning as expected. This usually doesn't include UI (see End-to-end Tests for that) but small chunks of internal logic, for example individual functions or methods.

Rust

Cargo comes with a builtin test runner - cargo test - that will run unit tests and report passes and failures. The simplest test in rust is a function with the test attribute. To change a plain function into a test, add #[test] to the line before fn.

Since it's common to write many small tests to ensure different expectations, Rust unit tests are commonly grouped into Test Modules:


#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    #[test]
    fn first_test() {
        assert_eq!(2 + 2, 4);
    }

        #[test]
    fn second_test() {
        assert_eq!(4 + 4, 8);
    }
}
}
A simplified Rust test suite containing two very basic tests.

The #[cfg(test)] attribute ensures that the module is only compiled when running cargo test but stripped when you build the binary for development or release. To learn more, see the Conditional Compilation reference.

The function body uses the assert_eq! macro to assert that 2 + 2 equals 4 and 4 + 4 equals 8.

JavaScript

Unit tests in JavaScript are more complicated, as there are many competing test runners: Jest, Mocha, and Vitest are popular choices. For the following code-snippets we will be using Vitest.

Contrary to Rust, where tests co-located with the source code (i.e. in the same file), JavaScript tests are written in a separate file, commonly named *.tests.js.

Filename: tests/main.test.ts

import { expect, test } from "vitest";
import { foo } from "./main";

test("foo", () => {
  const data = foo();
  expect(data).toEqual("foo");
});
A simplified vitest test suite containing one very basic test.

As your tests are executed in an Isolated Context, you need to mock Tauri APIs. See Mocking Tauri APIs for more details.

A popular convention is to add a test script to your package.json file, so users immediately know how to test your application. Let's add a test script to our example application that just alises to the vitest test runner:

{
  "name": "test-application",
  "version": "1.0.0",
  "scripts": {
    "test": "vitest"
  },
  "devDependencies": {
    "vitest": "0.3.5"
  }
}

Now we can run our test suite by opening a terminal and executing the following command:

npm

npm test
yarn test
pnpm test

End-to-End Testing

End-to-end Tests (E2E) tests simulate a user’s step-by-step experience, testing the interactions between many components in the process.

WebDriver is a standardized interface to interact with web documents primarily intended for automated testing. It provides capabilities for navigating to web pages, user input, JavaScript execution, and more.

Tauri supports the WebDriver interface by leveraging the native platform's WebDriver server underneath a cross-platform wrapper provided by tauri-driver.

Prerequisites

Install the latest tauri-driver or update an existing installation by running:

cargo install tauri-driver

Because we currently utilize the platform's native WebDriver server, there are some requirements for running tauri-driver on supported platforms. Platform support is currently limited to Linux and Windows.

Linux

We use WebKitWebDriver on Linux platforms. Check if this binary exists already (command which WebKitWebDriver) as some distributions bundle it with the regular WebKit package. Other platforms may have a separate package for them, such as webkit2gtk-driver on Debian-based distributions.

Windows

Make sure to grab the version of Microsoft Edge Driver that matches your Windows' Edge version that the application is being built and tested on. This should almost always be the latest stable version on up-to-date Windows installs. If the two versions do not match, you may experience your WebDriver testing suite hanging while trying to connect.

The download contains a binary called msedgedriver.exe. tauri-driver looks for that binary in the $PATH so make sure it's either available on the path or use the --native-driver option on tauri-driver. You may want to download this automatically as part of the CI setup process to ensure the Edge, and Edge Driver versions stay in sync on Windows CI machines. A guide on how to do this may be added at a later date.

With WebdriverIO

WebdriverIO (WDIO) is a test automation framework that provides a Node.js package for testing with WebDriver. Its ecosystem also includes various plugins (e.g. reporter and services) that can help you put together your test setup.

Install the test runner

Open a terminal and run the the WebdriverIO starter toolkit in your project with the following command:

npm

npx wdio .

yarn

yarn create wdio .

pnpm

pnpm create wdio .

This installs all necessary packages for you and generates a wdio.conf.js configuration file.

Connect your Tauri app

Update the wdio.conf.js file with the following options:

// keep track of the `tauri-driver` child process
let tauriDriver;

exports.config = {
  // ...
  // ensure the rust project is built
  // since we expect this binary to exist
  // for the webdriver sessions
  onPrepare: () => spawnSync("cargo", ["build", "--release"]),

  // ensure we are running `tauri-driver` before the session starts
  // so that we can proxy the webdriver requests
  beforeSession: () =>
    (tauriDriver = spawn(
      path.resolve(os.homedir(), ".cargo", "bin", "tauri-driver"),
      [],
      { stdio: [null, process.stdout, process.stderr] }
    )),

  // clean up the `tauri-driver` process we spawned
  afterSession: () => tauriDriver.kill(),
  // ...
};
Example WebdriverIO config that launches a Tauri app before tests are run and kills the app after all tests finished.

Add tests

Let's add a test file and a couple e2e tests to show what WDIO is capabable of. The test runner will load these files and autimatically run them.

Filename: test/specs/example.e2e.js

// calculates the luma from a hex color `#abcdef`
function luma(hex) {
  if (hex.startsWith("#")) {
    hex = hex.substring(1);
  }

  const rgb = parseInt(hex, 16);
  const r = (rgb >> 16) & 0xff;
  const g = (rgb >> 8) & 0xff;
  const b = (rgb >> 0) & 0xff;
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

describe("Hello Tauri", () => {
  it("should be cordial", async () => {
    const header = await $("body > h1");
    const text = await header.getText();

    expect(text).toMatch(/^[hH]ello/);
  });

  it("should be excited", async () => {
    const header = await $("body > h1");
    const text = await header.getText();

    expect(text).toMatch(/!$/);
  });

  it("should be easy on the eyes", async () => {
    const body = await $("body");
    const backgroundColor = await body.getCSSProperty(
      "background-color"
    );

    expect(luma(backgroundColor.parsed.hex)).toBeLessThan(100);
  });
});
Listing 2-TODO: An example test suite using WebdriverIO that asserts various DOM properties.

The luma function on top is just a helper function for one of our tests and is not related to the actual testing of the application. If you are familiar with other testing frameworks, you may notice similar functions being exposed that are used, such as describe, it, and expect. The other APIs, such as items like $ and its exposed methods, are covered by the WebdriverIO API docs.

Run your tests

To run your test suite, open a terminal and execute the following command:

npm

npx wdio run wdio.conf.json

yarn

yarn wdio run wdio.conf.json

pnpm

pnpm wdio run wdio.conf.json

You should see the following output:

➜  webdriverio git:(main) ✗ yarn test
yarn run v1.22.11
$ wdio run wdio.conf.js

Execution of 1 workers started at 2021-08-17T08:06:10.279Z

[0-0] RUNNING in undefined - /test/specs/example.e2e.js
[0-0] PASSED in undefined - /test/specs/example.e2e.js

 "spec" Reporter:
------------------------------------------------------------------
[wry 0.12.1 linux #0-0] Running: wry (v0.12.1) on linux
[wry 0.12.1 linux #0-0] Session ID: 81e0107b-4d38-4eed-9b10-ee80ca47bb83
[wry 0.12.1 linux #0-0]
[wry 0.12.1 linux #0-0] » /test/specs/example.e2e.js
[wry 0.12.1 linux #0-0] Hello Tauri
[wry 0.12.1 linux #0-0]    ✓ should be cordial
[wry 0.12.1 linux #0-0]    ✓ should be excited
[wry 0.12.1 linux #0-0]    ✓ should be easy on the eyes
[wry 0.12.1 linux #0-0]
[wry 0.12.1 linux #0-0] 3 passing (244ms)
Listing 2-TODO: Example output from WebdriverIO showing the 3 tests from earlier passing.

With Selenium

Selenium is a web automation framework that exposes bindings to WebDriver APIs in many languages. Their Node.js bindings are available under the selenium-webdriver package on NPM. Unlike the WebdriverIO Test Suite, Selenium does not come out of the box with a Test Suite and leaves it up to the developer to provide one. We chose Mocha for this example, since it's a popular choice.

Install the test runner

npm

npm install mocha chai selenium-webdriver

yarn

yarn add -D mocha chai selenium-webdriver

pnpm

pnpm add -D mocha chai selenium-webdriver

Connect your Tauri app

The following code will start an instance of your app before tests are run and ensure the instance is terminated afterwards. Let's add it to the default mocha testing file:

Filename: test/test.js

const os = require("os");
const path = require("path");
const { spawn, spawnSync } = require("child_process");
const { Builder, By, Capabilities } = require("selenium-webdriver");

// create the path to the expected application binary
const application = path.resolve(
  __dirname,
  "..",
  "..",
  "..",
  "target",
  "release",
  "hello-tauri-webdriver"
);

// keep track of the webdriver instance we create
let driver;

// keep track of the tauri-driver process we start
let tauriDriver;

before(async function () {
  // set timeout to 2 minutes
  // to allow the program to build if it needs to
  this.timeout(120000);

  // ensure the program has been built
  spawnSync("cargo", ["build", "--release"]);

  // start tauri-driver
  tauriDriver = spawn(
    path.resolve(os.homedir(), ".cargo", "bin", "tauri-driver"),
    [],
    { stdio: [null, process.stdout, process.stderr] }
  );

  const capabilities = new Capabilities();
  capabilities.set("tauri:options", { application });
  capabilities.setBrowserName("wry");

  // start the webdriver client
  driver = await new Builder()
    .withCapabilities(capabilities)
    .usingServer("http://localhost:4444/")
    .build();
});

after(async function () {
  // stop the webdriver session
  await driver.quit();

  // kill the tauri-driver process
  tauriDriver.kill();
});
Listing 2-TODO: Example Selenium file that launches a Tauri app before tests are run and kills the app after all tests finished.

Add tests

Now we can add tests to the file we created earlier. We will be using assertion functions provided by Chai such as expect to validate our app works as expected.

Filename: test/test.js

const { expect } = require("chai");

describe("Hello Tauri", () => {
  it("should be cordial", async () => {
    const text = await driver
      .findElement(By.css("body > h1"))
      .getText();

    expect(text).to.match(/^[hH]ello/);
  });

  it("should be excited", async () => {
    const text = await driver
      .findElement(By.css("body > h1"))
      .getText();

    expect(text).to.match(/!$/);
  });

  it("should be easy on the eyes", async () => {
    // selenium returns color css values as rgb(r, g, b)
    const text = await driver
      .findElement(By.css("body"))
      .getCssValue("background-color");

    const rgb = text.match(
      /^rgb\((?<r>\d+), (?<g>\d+), (?<b>\d+)\)$/
    ).groups;

    expect(rgb).to.have.all.keys("r", "g", "b");

    const luma = 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b;
    expect(luma).to.be.lessThan(100);
  });
});
Listing 2-TODO: Example tests using Selenium and Chai to assert various DOM properties.

If you are familiar with JS testing frameworks, describe, it, and expect should look familiar. We also have semi-complex before() and after() callbacks to setup and teardown mocha. If you compare this to the WebdriverIO example, you notice a lot more code that isn't tests, as we have to set up a few more WebDriver related items.

Run your tests

To run your test suite, open a terminal and execute the following command:

npm

npx mocha

yarn

yarn mocha

pnpm

pnpm mocha

We should see output the following output:

➜  selenium git:(main) ✗ yarn test
yarn run v1.22.11
$ Mocha


  Hello Tauri
    ✔ should be cordial (120ms)
    ✔ should be excited
    ✔ should be easy on the eyes


  3 passing (588ms)

Done in 0.93s.
Listing 2-TODO: Output from Selenium showing the 3 earlier tests passing.

Mocking Tauri APIs

When writing your frontend tests, having a "fake" Tauri environment to simulate windows or intercept IPC calls is common, so-called mocking. The @tauri-apps/api/mocks module provides some helpful tools to make this easier for you:

Remember to clear mocks after each test run to undo mock state changes between runs! See clearMocks() docs for more info.

IPC Requests

Most commonly, you want to intercept IPC requests; this can be helpful in a variety of situations:

  • Ensure the correct backend calls are made
  • Simulate different results from backend functions

Tauri provides the mockIPC function to intercept IPC requests. You can find more about the specific API in detail here.

The following examples use Vitest, but you can use any other frontend testing library such as jest.

import { beforeAll, expect, test } from "vitest";
import { randomFillSync } from "crypto";

import { mockIPC } from "@tauri-apps/api/mocks";
import { invoke } from "@tauri-apps/api/tauri";

// jsdom doesn't come with a WebCrypto implementation
beforeAll(() => {
  window.crypto = {
    getRandomValues: function (buffer) {
      return randomFillSync(buffer);
    },
  };
});

test("invoke simple", async () => {
  mockIPC((cmd, args) => {
    // simulate rust command called "add"
    if (cmd === "add") {
      return args.a + args.b;
    }
  });

  expect(invoke("add", { a: 12, b: 15 })).resolves.toBe(27);
});
Listing 2-TODO: Vitest test file showing a mocked command handler that simulates a simple add function.

Sometimes you want to track more information about an IPC call; how many times was the command invoked? Was it invoked at all? You can use mockIPC() with other spying and mocking tools to test this:

import { beforeAll, expect, test, vi } from "vitest";
import { randomFillSync } from "crypto";

import { mockIPC } from "@tauri-apps/api/mocks";
import { invoke } from "@tauri-apps/api/tauri";

// jsdom doesn't come with a WebCrypto implementation
beforeAll(() => {
  //@ts-ignore
  window.crypto = {
    getRandomValues: function (buffer) {
      return randomFillSync(buffer);
    },
  };
});

test("invoke", async () => {
  mockIPC((cmd, args) => {
    // simulate rust command called "add"
    if (cmd === "add") {
      return args.a + args.b;
    }
  });

  // we can use the spying tools provided by vitest
  // to track the mocked function
  const spy = vi.spyOn(window, "__TAURI_IPC__");

  expect(invoke("add", { a: 12, b: 15 })).resolves.toBe(27);
  expect(spy).toHaveBeenCalled();
});

Listing 2-TODO: The mocked __TAURI_IPC__ is compatible with existing testing tools.

Windows

Sometimes you have window-specific code (a splash screen window, for example), so you need to simulate different windows. You can use the mockWindows() method to create fake window labels. The first string identifies the "current" window (i.e., the window your JavaScript believes itself in), and all other strings are treated as additional windows.

mockWindows() only fakes the existence of windows but no window properties. To simulate window properties, you need to intercept the correct calls using mockIPC()

import { beforeAll, expect, test } from "vitest";
import { randomFillSync } from "crypto";

import { mockWindows } from "@tauri-apps/api/mocks";

// jsdom doesn't come with a WebCrypto implementation
beforeAll(() => {
  //@ts-ignore
  window.crypto = {
    getRandomValues: function (buffer) {
      return randomFillSync(buffer);
    },
  };
});

test("invoke", async () => {
  mockWindows("main", "second", "third");

  const { getCurrent, getAll } = await import(
    "@tauri-apps/api/window"
  );

  expect(getCurrent()).toHaveProperty("label", "main");
  expect(getAll().map((w) => w.label)).toEqual([
    "main",
    "second",
    "third",
  ]);
});
Listing 2-TODO: A vitest test file with 3 mocked windows.

Virtual Machines

Tauri allows you to create cross-platform applications from a single codebase with ease.
This, however, doesn't mean you can just package a Website with Tauri and be good to go. Your end-users may run a number of Operating Systems (OS) on a wide variety of hardware. And they all expect your app to run well on their machine, "feel native" and integrate with OS features.

Here is where Virtual Machines (VMs) come into play: They can simulate a variety of CPU architectures, Peripherals and run any Operating System right from the convenience of your main development machine. This allows you to switch between different Operating Systems very quickly, to test and develop features and them.

The following picture shows a Windows 10, Windows 11, Ubuntu and macOS VM running inside a macOS host through UTM a VM software for macOS.

Windows 10, Windows 11, Ubuntu and macOS VM windows
Listing 2-TODO: Windows 10, Windows 11, Ubuntu and macOS VMs running inside a macOS host through [UTM] a VM software for macOS.

VM Software

There are many Virtual Machine Solutions available; most of them are free and Open-Source with some paid solutions.
And since giving detailed guides for each would blow the scope of this document, we give you a shortlist of popular projects below and invite you to read their respective documentation for more details.

  • VirtualBox Popular open-source VM software developed by Oracle.

  • VMware Paid VM software for Windows with a feature-limited free version.

  • Hyper-V Hardware virtualization technology built into Windows itself.

  • QEMU Open-source virtualization and emulation software predominantly on Linux and macOS.

  • UTM Easy to use virtualization and emulation on macOS using Apple Silicon features to run at near native speeds. Uses QEMU under the hood.

Building your Application

The Tauri bundler is part of the Tauri CLI and lets you compile your binary, package assets, and prepare a final bundle with a single command:

tauri build

Like the tauri dev command, the first time you run this, it takes some time to collect the Rust crates and build everything - but on subsequent runs, it only needs to rebuild your app's code, which is much quicker. Besides compiling the Rust project, the tauri build command does several other things for you:

  1. Build the Frontend

    If you have configured your tauri.conf.json correctly, the bundler calls the
    beforeBuildCommand during this step, allowing you to build your Frontend.

  2. Build the Rust Binary

    The bundler calls cargo build under the hood and compile the Rust project into a single executable. This step also inlines your previously generated Frontend files into the executable. The compiled executable is placed in the src-tauri/target/release folder.

  3. Create Packages

    During this step the bundler collects all necessary files for packaging: the binary, resources, sidecars, icons and app manifests. These files will be packaged up according to the package formats your operating system supports. The created artifacts are located in the src-tauri/target/release/bundle/ folder.

  4. Code Sign

    If you have code-signing enabled, either for Windows, macOS, or the Updater, the last step is signing the created artifacts. This step will create .sig files in the src-tauri/target/release/bundle/ for each supported packaging format.

Packaging Formats

It will detect your operating system and build a bundle accordingly. It currently supports:

  • Windows: .exe, .msi, .msi.zip (updater)
  • macOS: .app, .dmg, .app.tar.gz (updater)
  • Linux: .deb, .AppImage, .AppImage.tar.gz (updater)

Configuration

There are a number of config options that change how the build process works. For configuring the platform-specific packages, see Building: Windows Installer, Building: macOS Bundle, Building: Linux and Building: Updater Artifacts respectively.

tauri.bundle.active

Set to false to disable the bundling process. This will still compile the Rust project, but not produce any platform-specific packages.

Resources

Resources are configured by the tauri.bundle.resources property and are a convenient way to include files or folders that should not be inlined into the executable but kept on the filesystem. A common use case is supporting files for sidecars or images or videos.

Cross-Platform Compilation

Cross-platform compilation is not supported at this moment. If you want to produce binaries for all three operating systems, you can use Virtual Machines or a CI service like GitHub Actions.

Linux

Tauri applications for Linux are distributed either as Debian Packages (.deb) or AppImages (.AppImage). This guides provides information about format specific quirks and customization opportunities.

AppImage

AppImage is a distribution format that does not rely on the system installed packages and instead bundles all dependencies and files needed by the application. For this reason, the output file is larger but easier to distribute since it is supported on many Linux distributions and can be executed without installation, just making the file executable (chmod a+x MyProject.AppImage) and running it (./MyProject.AppImage).

AppImages are convenient, simplifying the distribution process if you cannot make a package targeting the distribution's package manager. Still, you should carefully use it as the file size grows from the 2-6MBs range to 70+MBs.

Debian Package

Debian packages are a compressed collection of files installed on various Linux distributions. Unlike AppImages they don't bundle required libraries, relying instead on the correct dependency versions installed on the system. This makes them less portable and reliable since missing libraries or incompatible versions will cause problems. Debian packages are recommended only for distributions that have no support for AppImages.

Bootstrapper

Instead of launching the app directly, you can configure the bundled app to run a script that tries to expose the environment variables to the app; without that, you'll have trouble using system programs because the PATH environment variable isn't correct. You can enable it with the tauri.bundle.deb.useBootstrapper config.

Additional Files

The Debian package allows you to specify additional files that copied to the the user's filesystem upon installation. The configuration object maps

{
  "tauri": {
    "bundle": {
      "deb": {
        "files": {
          // copies the README.md file to /usr/lib/README.md
          "/usr/lib/README.md": "../README.md",
          // copies the entire public directory to /usr/lib/assets
          "usr/lib/assets": "../public/"
        }
      }
    }
  }
}
Listing 3-TODO:

Windows Installer

Tauri applications for Windows are distributed as Microsoft Installer (.msi files). The Tauri CLI bundles your application binary and additional resources in this format if you build on windows. This guide provides information about available customization options for the installer.

32-bit Windows

The Tauri CLI compiles your executable using your machine's architecture by default. Assuming that you're developing on a 64-bit machine, the CLI will produce 64-bit applications. If you need to support 32-bit machines, you can compile your application with a different Rust target using the --target flag:

tauri build --target i686-pc-windows-msvc
Listing 3-TODO: Building a Tauri application for 32-bit windows.

By default Rust only installs toolschains for your machine's target, so you need to install the 32-bit Windows toolchain first: rustup target add i686-pc-windows-msvc. You can get a full list of Rust targets by running rustup target list.

Using a Fixed Version of the Webview2 Runtime

By default, the Tauri installer downloads and installs the Webview2 Runtime if it is not already installed (On Windows 11, WebView2 is preinstalled).

You can remove the Webview2 Runtime download check from the installer by setting tauri.bundle.windows.wix.skipWebviewInstall to true. Your application WON'T work if the user does not have the runtime installed.

Using a global installation of WebView2 is great for security as Window keeps it updated, but if your end-users have no internet connection or you need a particular version of WebView2, Tauri can bundle the runtime files for you. Keep in mind, that this increases the size of windows installers by 150MB since your app will include its own copy of chromium.

  1. Download the Webview2 fixed version runtime from the official website, a .cab file for the selected architecture. In this example, the downloaded filename is
    Microsoft.WebView2.FixedVersionRuntime.98.0.1108.50.x64.cab

  2. Extract the file to the core folder:
    Expand .\Microsoft.WebView2.FixedVersionRuntime.98.0.1108.50.x64.cab -F:* ./src-tauri

  3. Configure the Webview2 runtime path on tauri.conf.json:

    {
      "tauri": {
        "bundle": {
          "windows": {
            "webviewFixedRuntimePath": "./Microsoft.WebView2.FixedVersionRuntime.98.0.1108.50.x64/"
          }
        }
      }
    }
    
  4. Run tauri build to produce the Windows Installer with the fixed Webview2 runtime.

Customizing the Installer

The Windows Installer package is built using the WiX Toolset v3. Currently, you can change it by using a custom WiX source code (an XML file with a .wxs file extension) or through WiX fragments.

Replacing the Installer Code with a Custom WiX File

The Windows Installer XML defined by Tauri is configured to work for the common use case of simple webview-based applications; you can find it here. It uses handlebars so the Tauri CLI can brand your installer according to your tauri.conf.json definition. If you need a completely different installer, a custom template file can be configured on tauri.bundle.windows.wix.template.

Extending the Installer with WiX Fragments

A WiX fragment is a container where you can configure almost everything offered by WiX. In this example, we will define a fragment that writes two registry entries:

<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
  <Fragment>
    <!-- these registry entries should be installed
		 to the target user's machine -->
    <DirectoryRef Id="TARGETDIR">
      <!-- groups together the registry entries to be installed -->
      <!-- Note the unique `Id` we provide here -->
      <Component Id="MyFragmentRegistryEntries" Guid="*">
        <!-- the registry key will be under
			 HKEY_CURRENT_USER\Software\MyCompany\MyApplicationName -->
        <!-- Tauri uses the second portion of the
			 bundle identifier as the `MyCompany` name
			 (e.g. `tauri-apps` in `com.tauri-apps.test`)  -->
        <RegistryKey
          Root="HKCU"
          Key="Software\MyCompany\MyApplicationName"
          Action="createAndRemoveOnUninstall"
        >
          <!-- values to persist on the registry -->
          <RegistryValue
            Type="integer"
            Name="SomeIntegerValue"
            Value="1"
            KeyPath="yes"
          />
          <RegistryValue Type="string" Value="Default Value" />
        </RegistryKey>
      </Component>
    </DirectoryRef>
  </Fragment>
</Wix>

Save the fragment file with the .wxs extension somewhere in your project and reference it on tauri.conf.json:

{
  "tauri": {
    "bundle": {
      "windows": {
        "wix": {
          "fragmentPaths": ["./path/to/registry.wxs"],
          "componentRefs": ["MyFragmentRegistryEntries"]
        }
      }
    }
  }
}

Note that ComponentGroup, Component, FeatureGroup, Feature and Merge element ids must be referenced on the wix object of tauri.conf.json on the componentGroupRefs, componentRefs, featureGroupRefs, featureRefs and mergeRefs respectively in order to be included on the installer.

Internationalization

The Windows Installer is built using the en-US language by default. Internationalization (i18n) can be configured using the tauri.bundle.windows.wix.language property, defining the languages Tauri should build an installer against. You can find the language names to use in the Language-Culture column here.

Compiling an Installer for a single Language

To create a single installer targeting a specific language, set the language value to a string:

{
  "tauri": {
    "bundle": {
      "windows": {
        "wix": {
          "language": "fr-FR"
        }
      }
    }
  }
}

Compiling an Installer for each Language in a List

To compile an installer targeting a list of languages, use an array. A specific installer for each language will be created, with the language key as a suffix:

{
  "tauri": {
    "bundle": {
      "windows": {
        "wix": {
          "language": ["en-US", "pt-BR", "fr-FR"]
        }
      }
    }
  }
}

Configuring the Installer for each Language

A configuration object can be defined for each language to configure localization strings:

{
  "tauri": {
    "bundle": {
      "windows": {
        "wix": {
          "language": {
            "en-US": null,
            "pt-BR": {
              "localePath": "./wix/locales/pt-BR.wxl"
            }
          }
        }
      }
    }
  }
}

The localePath property defines the path to a language file, a XML configuring the language culture:

<WixLocalization
  Culture="en-US"
  xmlns="http://schemas.microsoft.com/wix/2006/localization"
>
  <String Id="LaunchApp"> Launch MyApplicationName </String>
  <String Id="DowngradeErrorMessage">
    A newer version of MyApplicationName is already installed.
  </String>
  <String Id="PathEnvVarFeature">
    Add the install location of the MyApplicationName executable to
    the PATH system environment variable. This allows the
    MyApplicationName executable to be called from any location.
  </String>
  <String Id="InstallAppFeature">
    Installs MyApplicationName.
  </String>
</WixLocalization>

The WixLocalization element's Culture field must match the configured language.

Currently Tauri references the following locale strings: LaunchApp, DowngradeErrorMessage, PathEnvVarFeature and InstallAppFeature. You can define your own strings and reference them on your custom template or fragments with "!(loc.TheStringId)". See the WiX localization documentation for more information.

macOS Bundle

Tauri applications for macOS are distributed either with an Application Bundle (.app files) or an Apple Disk Image (.dmg files). The Tauri CLI automatically bundles your application code in these formats, providing options to codesign and notarize your application.

Setting a Minimum System Version

The minimum version of the operating system required for a Tauri app to run in macOS is 10.13. If you need support for newer macOS APIs like window.print that is only supported from macOS version 11.0 onwards, you can change the tauri.bundle.macOS.minimumSystemVersion. This will in turn set the Info.plist LSMinimumSystemVersion property and the MACOSX_DEPLOYMENT_TARGET environment variable.

Note: macOS High Sierra (10.13) no longer receives security updates from Apple. You should target macOS Catalina (10.15) if possible.

Binary Targets

macOS applications can target Apple Silicon, Intel-based Mac computers, or universal macOS binaries that work on both architectures. By default, the Tauri CLI uses your machine's architecture, but you can configure a different target using the --target flag:

tauri build --target aarch64-apple-darwin

Supported targets are:

  • aarch64-apple-darwin - Apple silicon, also known as m1 Macs, for all models introduced after late 2020.
  • x86_64-apple-darwin - Intel-based Macs, all Macs introduced before fall 2020.
  • universal-apple-darwin - produces a Universal macOS Binary that runs on both Apple silicon and Intel-based Macs.

While Apple silicon machines can run applications compiled for Intel-based Macs through a translation layer called Rosetta, they tend to suffer from performance problems. It is common practice to let the user choose the correct target when downloading the app, but you can also choose to distribute a Universal Binary. Universal Binaries include both aarch64 and x86_64 executables, giving you the best experience on both architectures. Note, however, that this increases your bundle size significantly.

Application Bundle Customization

The Tauri configuration file provides the following options to customize your application bundle:

  • Bundle name - Your apps human-readable name. Configured by the package.productName property.
  • Bundle version - Your apps version. Configured by the package.version property.
  • Application category - The category that describes your app. Configured by the tauri.bundle.category property. You can see a list of macOS categories here.
  • Copyright - A copyright string associated with your app. Configured by the tauri.bundle.copyright property.
  • Bundle icon - Your apps icon. Uses the first .icns file listed on the tauri.bundle.icon array.
  • Minimum system version - Configured by the tauri.bundle.macOS.minimumSystemVersion property.
  • DMG license file - A license that is added to the .dmg file. Configure by the tauri.bundle.macOS.license property.
  • Entitlements.plist file - Entitlements control what APIs your app will have access to. Configured by the tauri.bundle.macOS.entitlements property.
  • Exception domain - an insecure domain that your application can access such as a localhost or a remote http domain. It is a convenience configuration around NSAppTransportSecurity > NSExceptionDomains setting NSExceptionAllowsInsecureHTTPLoads and NSIncludesSubdomains to true. See tauri.bundle.macOS.exceptionDomain.
  • Bootstrapper - Instead of launching the app directly, you can configure the bundled app to run a script that tries to expose the environment variables to the app; without that, you'll have trouble using system programs because the PATH environment variable isn't correct. Enable it with tauri.bundle.macOS.useBootstrapper.

These options generate the application bundle Info.plist file. You can extend the generated file with your own Info.plist file stored on the Tauri folder (src-tauri by default). The CLI merges both .plist files on production, and the core layer embeds it on the binary on development.

Updater Artifacts

The Tauri bundler automatically generates update artifacts if the updater is enabled in tauri.conf.json Your update artifacts are automatically signed if the bundler can locate your private and public key.

The signature can be found in the sig file. The signature can be uploaded to GitHub safely or made public if your private key is secure.

macOS

On macOS, Tauri creates a .tar.gz from the whole application. (.app)

target/release/bundle
└── osx
    └── app.app
    └── app.app.tar.gz (update bundle)
    └── app.app.tar.gz.sig (if signature enabled)

Windows

On Windows, Tauri creates a .zip from the MSI; when downloaded and validated, we run the MSI install.

target/release
└── app.x64.msi
└── app.x64.msi.zip (update bundle)
└── app.x64.msi.zip.sig (if signature enabled)

Linux

On Linux, Tauri creates a .tar.gz from the AppImage.

target/release/bundle
└── appimage
    └── app.AppImage
    └── app.AppImage.tar.gz (update bundle)
    └── app.AppImage.tar.gz.sig (if signature enabled)

Reducing the App Size

With Tauri, we are working to reduce the environmental footprint of applications by using system resources where available, providing compiled systems that don't need runtime evaluation, and offering guides so that engineers can go even smaller without sacrificing performance or security. The point is, by saving resources, we are doing our part to help you help us save the planet -- which is the only bottom line that companies in the 21st Century should care about.

So if you are interested in learning how to improve your app size and performance, read on!

You can't improve what you can't measure

Before you can optimize your app, you need to figure out what takes up space in your app! Here are a couple of tools that can assist you with that:

  • cargo-bloat - A rust utility to determine what takes the most space in your app. It gives you an excellent, sorted overview of the most significant rust functions.

  • cargo-expand - Macros make your rust code more concise and easier to read, but they are also hidden size traps! Use cargo-expand to see what those macros generate under the hood.

  • rollup-plugin-visualizer - A tool that generates beautiful (and insightful) graphs from your rollup bundle. Very convenient for figuring out what JavaScript dependencies contribute to your final bundle size the most.

  • rollup-plugin-graph - You noticed a dependency included in your final frontend bundle, but you are unsure why? rollup-plugin-graph generates graphviz compatible visualizations of your entire dependency graph.

These are just a couple of tools that you might use. Make sure to check your frontend bundlers plugin list for more!

Checklist

1. Minify Javascript

Why?

JavaScript makes up a large portion of a typical Tauri app, so it's important to make the JavaScript as lightweight as possible.

How?

You can choose among a plethora of JavaScript bundlers; popular choices are Vite, webpack, and rollup. All of them can produce minified JavaScript if configured correctly, so please consult your bundler documentation for specific options. Generally speaking; however, you should make sure to:

  • Enable tree shaking

    This option removes unused JavaScript from your bundle. All popular bundlers enable this by default.

  • Enable minification

    Minification removes unnecessary whitespace, shortens variable names, and applies other optimizations. Most bundlers enable this by default; a notable exception is rollup, where you need plugins like rollup-plugin-terser or rollup-plugin-uglify.

    Note: You can use minifiers like terser and esbuild as standalone tools.

  • Disable source maps

    Source maps provide a pleasant developer experience when working with languages that compile to JavaScript, such as TypeScript. As source maps tend to be quite large, you must disable them when building for production. They have no benefit to your end-user, so it's effectively dead weight.

2. Optimize Dependencies

Many popular libraries have smaller and faster alternatives that you can choose instead.

Why?

Most libraries you use depend on many libraries themselves, so a library that looks inconspicuous at first glance might add several megabytes worth of code to your app.

How?

You can use Bundlephobia to find the cost of JavaScript dependencies. Inspecting the cost of rust dependencies is generally harder since the compiler does many optimizations.

If you find a library that seems excessively large, google around, chances are someone else already had the same thought and created an alternative. A good example is Moment.js and it's Many alternatives.

But keep in mind: The best dependency is no dependency, meaning that you should always prefer language builtins over 3rd party packages.

3. Optimize Images

Why?

According to the Http Archive, images are the biggest contributor to website weight. So if your app includes have background images or icons, make sure to optimize them!

How?

You can choose between a variety of manual options (GIMP, Photoshop, Squoosh) or plugins for your favorite frontend build tools (vite-imagetools, vite-plugin-imagemin, image-minimizer-webpack-plugin).

The imagemin library most of the plugins use is officially unmaintained.

  • Use modern image formats

    Formats such as webp or avif offer size reductions of up to 95% compared to jpeg while maintaining excellent visual accuracy. You can use tools such as Squoosh to try different formats on your images.

  • Size images accordingly

    No one appreciates you shipping the 6K raw image with your app, so make sure to size your image accordingly. Images that appear large on-screen should be sized larger than images that take up less screen space.

  • Don't use Responsive Images

    In a Web Environment, you are supposed to use Responsive Images to load the correct image size for each user dynamically. You are not building a simple website, though: All your images are already downloaded. So using Responsive Images only bloat your app with redundant copies.

  • Remove Metadata

    Images that were taken straight from a camera or stock photo side often include metadata about the Camera and Lens model or Photographer. Not only are those wasted bytes, but metadata properties can also hold potentially sensitive information such as the time, day, and location of the photo.

4. Remove Unnecessary Custom Fonts

Consider not shipping custom fonts with your app and relying on system fonts instead. If you must ship custom fonts, make sure they are in modern, optimized formats such as woff2.

Why?

Fonts can be pretty big, so using the fonts already included in the Operating System reduces the footprint of your app. It also avoids FOUT (Flash of Unstyled Text) and makes your app feel more "native" since it uses the same font as all other apps.

If you must include custom fonts, make sure you include them in modern formats such as woff2 as those tend to be way smaller than legacy formats.

How?

Use so-called "System Font Stacks" in your CSS. There are a number of variations, but here are 3 basic ones to get you started:

  • Sans-serif

    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
      Helvetica, Arial, sans-serif, "Apple Color Emoji",
      "Segoe UI Emoji";
    
  • Serif

    font-family: Iowan Old Style, Apple Garamond, Baskerville,
      Times New Roman, Droid Serif, Times, Source Serif Pro, serif, Apple
        Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
    
  • Monospace

    font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
      Liberation Mono, monospace;
    

5. Allowlist Config

You can reduce the size of your app by only enabling the Tauri API features you need in the allowlist config.

Why?

The allowlist config determines what API features to enable; disabled features will not be compiled into your app. This is an easy way of shedding some extra weight.

How?

An example from a typical tauri.conf.json:

{
  "tauri": {
    "allowlist": {
      "all": false,
      "fs": {
        "writeFile": true
      },
      "shell": {
        "execute": true
      },
      "dialog": {
        "save": true
      }
    }
  }
}

6. Rust Build-time Optimizations

Configure your cargo project to take advantage of rusts size optimization features. Why is a rust executable large ? provides an excellent explanation on why this matters and an in-depth walkthrough. At the same time, Minimizing Rust Binary Size is more up-to-date and has a couple of extra recommendations.

Why?

Rust is notorious for producing large binaries, but you can instruct the compiler to optimize the final executable's size.

How?

Cargo exposes several options that determine how the compiler generates your binary. The "recommended" options for Tauri apps are these:

[profile.release]
panic = "abort" # Strip expensive panic clean-up logic
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
lto = true # Enables link to optimizations
opt-level = "s" # Optimize for binary size

There is also opt-level = "z" available to reduce the resulting binary size. "s" and "z" can sometimes be smaller than the other, so test it with your application!

We've seen smaller binary sizes from "s" for Tauri example applications, but real-world applications can always differ.

For a detailed explanation of each option and a bunch more, refer to the Cargo books Profiles section.

Unstable Rust Compression Features

The following suggestions are all unstable features and require a nightly toolchain. See the Unstable Features documentation for more information on what this involves.

The following methods involve using unstable compiler features and require the rust nightly toolchain. If you don't have the nightly toolchain + rust-src nightly component added, try the following:

rustup toolchain install nightly
rustup component add rust-src --toolchain nightly

The Rust Standard Library comes precompiled. This means rust is faster to install, but also that the compiler can't optimize the Standard Library. You can apply the optimization options for the rest of your binary + dependencies to the std with an unstable flag. This flag requires specifying your target, so know the target triple you are targeting.

cargo +nightly build --release -Z build-std --target x86_64-unknown-linux-gnu

If you are using panic = "abort" in your release profile optimizations, you need to make sure the panic_abort crate is compiled with std. Additionally, an extra std feature can further reduce the binary size. The following applies both:

cargo +nightly build --release -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort --target x86_64-unknown-linux-gnu

See the unstable documentation for more details about -Z build-std and -Z build-std-features.

7. Stripping

Use strip utilities to remove debug symbols from your compiled app.

Why?

Your compiled app includes so-called "Debug Symbols" that include function and variable names. Your end-users will probably not care about Debug Symbols, so this is a pretty surefire way to save some bytes!

How?

The easiest way is to use the famous strip utility to remove this debugging information.

strip target/release/my_application

See your local strip manpage for more information and flags that can be used to specify what information gets stripped out from the binary.

Rust 1.59 now has a builtin version of strip!
It can be enabled by adding the following to your Cargo.toml:

[profile.release]
strip = true  # Automatically strip symbols from the binary.

8. UPX

UPX, Ultimate Packer for eXecutables, is a dinosaur amongst the binary packers. This 23-year old, well-maintained piece of kit is GPL-v2 licensed with a pretty liberal usage declaration. Our understanding of the licensing is that you can use it for any purposes (commercial or otherwise) without needing to change your license unless you modify the source code of UPX.

Why?

Maybe your target audience has very slow internet, or your app needs to fit on a tiny USB stick, and all the above steps haven't resulted in the savings you need. Fear not, as we have one last trick up our sleeves:

UPX compresses your binary and creates a self-extracting executable that decompresses itself at runtime.

How?

You should know that this technique might flag your binary as a virus on Windows and macOS - so use at your own discretion, and as always, validate with Frida and do real distribution testing!

Usage on macOS

brew install upx
yarn tauri build
upx --ultra-brute src-tauri/target/release/bundle/macos/app.app/Contents/macOS/app

                        Ultimate Packer for eXecutables
                            Copyright (C) 1996 - 2018
UPX 3.95        Markus Oberhumer, Laszlo Molnar & John Reiser   Aug 26th 2018

        File size         Ratio      Format      Name
    --------------------   ------   -----------   -----------
    963140 ->    274448   28.50%   macho/amd64   app

Windows Application Distribution

This guide provides information on code signing and uploading your app to the Windows Store.

Code Signing

Code signing your application lets users know that they downloaded the official executable of your app and not some malware that poses as your app. While it is not required, it improves users' confidence in your app.

The following list walks you through the necessary steps to code-sign a Windows application.

  1. Prerequisites

    This guide assumes you run Windows, either on a physical machine or a Virtual Machine and that you already have a working Tauri application.

  2. Get a Code Signing Certificate

    To sign your application, you need to acquire a Code Signing certificate from one of the supported certificate authorities like Digicert, Sectigo (formerly Comodo), or Godaddy.

    To eliminate all security prompts during installation, you need an extended validation (EV) code signing certificate. These certificates cost upwards of 400$ and require a hardware token. Depending on your country, they might also be sold to companies only.

  3. Create .pfx Certificate

    You need a PKCS 12 Certificate file to sign an executable, commonly called a PFX file. We will take the certificate file (e.g. cert.cer) and private key (e.g. private-key.key) you received from your certificate authority and convert them into a .pfx file.
    Open a PowerShell prompt and enter the following command:

    openssl pkcs12 -export -in cert.cer -inkey private-key.key -out certificate.pfx
    

    Make sure you don't forget the export password when prompted, we need it in the next step.

  4. Import Certificate

    You now need to import your newly created .pfx certificate into the Windows Keystore. First, we need to store the export password you previously created into an environment variable. The securest option is the Get-Credential helper; Enter the following command in your PowerShell prompt:

    $mypwd = Get-Credential `
             -UserName 'Enter password below' `
             -Message 'Enter password below'
    

    Next, you can use the Import-PfxCertificate command to actually import your .pfx file:

    Import-PfxCertificate `
             -FilePath C:\certificate.pfx `
             -CertStoreLocation Cert:\LocalMachine\My `
             -Password $mypwd.Password
    
  5. Tauri Configuration

    To configure Tauri for code signing we need to enter a few things into our tauri.conf.json file:

    • certificateThumbprint - The SHA-1 thumbprint of your certificate. Enter the following command and copy the values for localKeyID without spaces.

      openssl pkcs12 -info -in certificate.pfx
      

      For this example output the certificateThumbprint is
      A1B1A2B2A3B3A4B4A5B5A6B6A7B7A8B8A9B9A0B0.

      Bag Attributes
          localKeyID: A1 B1 A2 B2 A3 B3 A4 B4 A5 B5 A6 B6 A7 B7 A8 B8 A9 B9 A0 B0
      
    • digestAlgorithm - The SHA digest algorithm used for your certificate. This is likely sha256.

    • timestampUrl - A URL pointing to a timestamp server used to verify the time the certificate is signed. It's best to provide the timestamp server provided by your certificate authority here.

    • tsp - Enables the Time-Stamp Protocol (TSP, defined by RFC 3161) instead of NTP. Some certificate authorities, like SSL.com only provide TSP servers.

    "bundle": {
        "windows": {
                "certificateThumbprint": "A1B1A2B2A3B3A4B4A5B5A6B6A7B7A8B8A9B9A0B0",
                "digestAlgorithm": "sha256",
                "timestampUrl": "http://timestamp.comodoca.com",
                "tsp": false
        }
    }
    
  6. Sign your Application

    Now you can run tauri build and you will see the following additional output:

    info: signing app
    info: running signtool "C:\\Program Files (x86)\\Windows Kits\\10\\bin\\10.0.19041.0\\x64\\signtool.exe"
    info: Done Adding Additional Store
    Successfully signed: APPLICATION FILE PATH HERE
    

    And that's it! You have successfully signed your Tauri application!

Continous Integration

As the above-described process is rather laborious, most developers run this step as an automated part of their Continous Integration (CI). For users of GitHub Actions Tauri provides the Tauri Action, which simplifies the setup.

Note: The following example assumes you store the secret passwords and tokens using GitHub Secrets.

Filename: .github/workflows/publish.yml

name: "publish"
on:
  push:
    branches:
      - release

jobs:
  publish-tauri:
    strategy:
      fail-fast: false
      matrix:
        platform: [windows-latest]

    runs-on: ${{ matrix.platform }}
    steps:
      - uses: actions/checkout@v2
      - name: setup node
        uses: actions/setup-node@v1
        with:
          node-version: 12
      - name: install Rust stable
        uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
      - name: import windows certificate
        env:
          WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
          WINDOWS_CERTIFICATE_PASSWORD:
            ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
        run: |
          New-Item -ItemType directory -Path certificate
          Set-Content -Path certificate/tempCert.txt -Value $env:WINDOWS_CERTIFICATE
          certutil -decode certificate/tempCert.txt certificate/certificate.pfx
          Remove-Item -path certificate -include tempCert.txt
          Import-PfxCertificate `
              -FilePath certificate/certificate.pfx `
              -CertStoreLocation Cert:\LocalMachine\My `
              -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
      - name: install app dependencies
        run: yarn
      - uses: tauri-apps/tauri-action@v0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tagName: app-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version
          releaseName: "App v__VERSION__"
          releaseBody:
            "See the assets to download this version and install."
          releaseDraft: true
          prerelease: false
Listing 4-TODO: A GitHub Action workflow that builds, signs and publishes a Tauri application.

Submit Apps to the Windows Store

macOS Application Distribution

This guide provides information on code signing, notarizing and uploading your app to the Mac App Store.

If you are not utilizing GitHub Actions to perform builds of OSX DMGs, you will need to ensure the environment variable CI is set to true.
For more information refer to tauri-apps/tauri#592.

Code Signing

On macOS Catalina and later Gatekeeper enforces that you must sign and notarize your application. Unsigned software cannot be run, so contrary to Windows Code Signing this is not optional for macOS.

  1. Prerequisites

    This guide assumes you run Windows, either on a physical machine or a Virtual Machine, and that you already have a working Tauri application. You also need Xcode 11 or newer and an Apple Developer account enrolled in the Apple Developer Program.

  2. Get a Code Signing Certificate

    To create a new signing certificate, you must generate a Certificate Signing Request (CSR) file on your Mac computer. Create a certificate signing request describes guides you through creating a CSR.

    Next, open the Certificates, IDs & Profiles page and click on the Add button to open the interface to create a new certificate. Choose the appropriate certificate type (Apple Distribution to submit apps to the App Store, and Developer ID Application to ship apps outside the App Store). Upload your CSR, and the certificate will be created.

    Only the Apple Developer Account Holder can create Developer ID Application certificates. But it can be associated with a different Apple ID by creating a CSR with a different user email address.

  3. Downloading the Certificate

    On Certificates, IDs & Profiles page, click on the certificate you want to use and click the Download button. It saves a .cer file that installs the certificate on the keychain once opened. The name of the keychain entry represents the signing identity, which can also be found by running this command: security find-identity -v -p codesigning.

    A signing certificate is only valid if associated with your Apple ID. An invalid certificate won't be listed on the Keychain Access > My Certificates tab or the security find-identity -v -p codesigning output.

  4. Tauri Configuration

    To have the Tauri bundler sign your application, you need to configure it. This is done by setting a number of environment variables.

    Certificate environment variables

    • APPLE_SIGNING_IDENTITY - this is the signing identity we highlighted earlier. It must be defined to sign apps both locally and on CI machines.

    Additionally, to simplify the code signing process on CI, Tauri can automatically install the certificate on the keychain if you define the APPLE_CERTIFICATE and
    APPLE_CERTIFICATE_PASSWORD environment variables.

    1. Open the Keychain Access app and find your certificate's keychain entry.
    2. Expand the entry, double click on the key item, and select Export "$KEYNAME".
    3. Select the path to save the .p12 file and define the exported certificate password.
    4. Convert the .p12 file to base64 running the following script on the terminal: `
      openssl base64 -in /path/to/certificate.p12 -out certificate-base64.txt
      
    5. Set the contents of the certificate-base64.txt file to the APPLE_CERTIFICATE environment variable.
    6. Set the certificate password to the APPLE_CERTIFICATE_PASSWORD environment variable.

    Authentication environment variables

    • APPLE_ID and APPLE_PASSWORD - to authenticate with your Apple ID, set the APPLE_ID to your Apple account email (example: export APPLE_ID=tauri@icloud.com) and the APPLE_PASSWORD to an app-specific password for the Apple account.

    • APPLE_API_ISSUER and APPLE_API_KEY - alternatively, you can authenticate using an App Store Connect API key.
      Open the App Store Connect's Users and Access page, click the Add button and select a name and check Developer Access. The APPLE_API_ISSUER (Issuer ID) is presented above the keys table, and the APPLE_API_KEY is the value of the Key ID column of that table. You also need to download the private key, which can only be done once and is only visible after a page reload (the button is shown on the table row for the newly created key). The private key file must be saved in one of these location ./private_keys, ~/private_keys, ~/.private_keys or ~/.appstoreconnect/private_keys, as stated by xcrun altool --help.

  5. Sign your Application

    Now the Tauri bundler will sign and notarize your application automatically whenever you run tauri build.

    Congratulations! You have successfully signed your Tauri application!

Continous Integration

As the above-described process is rather laborious, most developers run this step as an automated part of their Continous Integration (CI). For users of GitHub Actions Tauri provides the Tauri Action, which simplifies the setup.

Note: The following example assumes you store the secret passwords and tokens using GitHub Secrets.

Filename: .github/workflows/publish.yml

name: "publish"
on:
  push:
    branches:
      - release

jobs:
  publish-tauri:
    strategy:
      fail-fast: false
      matrix:
        platform: [macos-latest]

    runs-on: ${{ matrix.platform }}
    steps:
      - uses: actions/checkout@v2
      - name: setup node
        uses: actions/setup-node@v2
        with:
          node-version: 12
      - name: install Rust stable
        uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
      - name: install app dependencies
        run: yarn
      - uses: tauri-apps/tauri-action@v0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
          APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
          APPLE_CERTIFICATE_PASSWORD:
            ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
          APPLE_SIGNING_IDENTITY:
            ${{ secrets.APPLE_SIGNING_IDENTITY }}
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
        with:
          tagName: app-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version
          releaseName: "App v__VERSION__"
          releaseBody:
            "See the assets to download this version and install."
          releaseDraft: true
          prerelease: false
Listing 4-TODO: A GitHub Action workflow that builds, signs, notarizes and publishes a Tauri application.

Submit Apps to the Mac App Store

After signing your application, you can now submit it to the Mac App Store. You should make sure your app adheres to Apple's requirements.

macOS Private APIs

If you have tauri.macOSPrivateApi enabled and make use of features like the transparent background or developer tools in production builds, your app can't be submitted to the Mac App Store.

Entitlements

Depending on the features you have enabled, you may need to request additional permissions by creating an entitlements.plist file. Use the tauri.bundle.macos.entitlements property to include the file in your final bundle.

Network access

Enable outgoing network connections to allow your app to connect to a server:

<key>com.apple.security.network.client</key>
<true/>

Enable incoming network connections to allow your app to open a network listening socket:

<key>com.apple.security.network.server</key>
<true/>

Updater

The updater is focused on making Tauri's application updates as safe and transparent as updates to a website.

Instead of publishing a feed of versions from which your app must select, Tauri updates to the version your server tells it to. This allows you to intelligently update your clients based on the request you give to Tauri. The server can remotely drive behaviors like rolling back or phased rollouts. The update JSON Tauri requests should be dynamically generated based on criteria in the request and whether an update is required. Tauri's installer is also designed to be fault-tolerant, and ensure that any updates installed are valid and safe.

Configuration

To enable updates you must add the following to your tauri.conf.json file:

"updater": {
    "active": true,
    "endpoints": [
        "https://releases.myapp.com/{{target}}/{{arch}}/{{current_version}}"
    ],
    "dialog": true,
    "pubkey": "YOUR_UPDATER_SIGNATURE_PUBKEY_HERE"
}

The required keys are active, endpoints and pubkey; others are optional. active must be a boolean. By default, it's set to false. endpoints must be an array. The strings {{current_version}} and {{target}} and {{arch}} are automatically replaced in the URL, allowing you to determine server-side if an update is available. If multiple endpoints are specified, the updater will fall back if a server is not responding within the pre-defined timeout. dialog must be a boolean if present. By default, it is set to true. If enabled, events are turned off as the updater handles everything. If you need the custom events, you MUST turn off the built-in dialog. pubkey must be a valid public-key generated with Tauri CLI. See Signing updates for details.

Update Requests

The Tauri updater will periodically send an HTTP GET request to the previously configured endpoints. The return type must be application/json and adhere to one of the following schemas.

Dynamic JSON Format

The dynamic response format allows you fine grained control over the update process. If the update server determines - based on the update request - that an update is necessary, it must respond with a status code of 200 OK and include valid update information of the following shape:

{
  "url": "https://mycompany.example.com/myapp/releases/myrelease.tar.gz",
  "version": "0.0.1",
  "notes": "Theses are some release notes",
  "pub_date": "2020-09-18T12:29:53+01:00",
  "signature": ""
}

The only required keys are url and version; all others are optional. pub_datemust be formated according to ISO 8601 if present and signature must be a valid signature generated with the Tauri Cli.

If no update is required your server must respond with a status code of 204 No Content.

Static JSON Format

To simplify the usage with static file hosting solutions, like AWS S3, Tauri supports an alternative static JSON response format. Tauri checks against the version field, and if the version is smaller than the current one and the platform is available, it triggers an update. The format of this file is detailed below:

{
  "version": "v1.0.0",
  "notes": "Test version",
  "pub_date": "2020-06-22T19:25:57Z",
  "platforms": {
    "darwin-aarch64": {
      "signature": "",
      "url": "https://github.com/tauri-apps/tauri-test/releases/download/v1.0.0/app-aarch64.app.tar.gz"
    },
    "darwin-x86_64": {
      "signature": "",
      "url": "https://github.com/tauri-apps/tauri-test/releases/download/v1.0.0/app-x86_64.app.tar.gz"
    },
    "linux-x86_64": {
      "signature": "",
      "url": "https://github.com/tauri-apps/tauri-test/releases/download/v1.0.0/app.AppImage.tar.gz"
    },
    "windows-x86_64": {
      "signature": "",
      "url": "https://github.com/tauri-apps/tauri-test/releases/download/v1.0.0/app.x64.msi.zip"
    },
    "windows-i686": {
      "signature": "",
      "url": "https://github.com/tauri-apps/tauri-test/releases/download/v1.0.0/app.x86.msi.zip"
    }
  }
}

The only required keys are version and platforms.<platform>.url; All others are optional. pub_datemust be formated according to ISO 8601 if present and
platforms.<platform>.signature must be a valid signature generated with the Tauri CLI.

Built-in Update Dialog

By default, the updater uses a built-in dialog API from Tauri.

New Update available

The dialog release notes are filled with the note property of the update response. If the user accepts, the update is downloaded and installed. Afterward, the user is prompted to restart the application.

Programmatic API

If you want to customize the dialog or customize the update experience in general, you may use the @tauri-apps/api/updater module to do so.

You need to disable built-in dialog. Otherwise, the javascript API will NOT work.

import { checkUpdate, installUpdate } from "@tauri-apps/api/updater";
import { relaunch } from "@tauri-apps/api/process";

try {
  const { shouldUpdate, manifest } = await checkUpdate();
  if (shouldUpdate) {
    // display dialog
    await installUpdate();
    // install complete, restart app
    await relaunch();
  }
} catch (error) {
  console.log(error);
}

The updater also emits a number of lifecycle events you may subscribe to:

tauri://update-available

Emitted when a new update is available, the event includes the following metadata:

version    Version announced by the server
date       Date announced by the server
body       Note announced by the server
window.listen("tauri://update-available".to_string(), move |msg| {
    println!("New version available: {:?}", msg);
});
import { listen } from "@tauri-apps/api/event";

listen("tauri://update-available", function (res) {
  console.log("New version available: ", res);
});
Listing 4-TODO: Listening to new update events from Rust and JavaScript.

tauri://update-status

Emitted while the update is downloaded and installed, you may use this to display a progress bar.

status    [ERROR/PENDING/DONE]
error     String/null

PENDING is emitted when the download is started and DONE when the install is complete. You can then ask to restart the application. ERROR is emitted when there is an error with the updater. We suggest listening to this event even if the dialog is enabled.

window.listen("tauri://update-status".to_string(), move |msg| {
    println!("New status: {:?}", msg);
});
import { listen } from "@tauri-apps/api/event";

listen("tauri://update-status", function (res) {
  console.log("New status: ", res);
});
Listing 4-TODO: Listening to update progress events from Rust and JavaScript.

Signing Updates

The updater offers built-in signature checking to ensure your update is authentic and can be safely installed. When present, the update response's signature field and the downloaded artifact will be checked against the configured pubkey using Minisign, a simple signature system using Ed25519 public-key signatures.

  1. Prerequisites

    This guide assumes you have the Tauri CLI and a working Tauri application.

  2. Generate Keypair

    To successfully sign and verify updates you need a Keypair, consisting of

    • A Public-Key (pubkey) - Used to verify the signatures. This key is safe to share with others and should be added to your tauri.conf.json.
    • A Private key (privkey) - Used to sign your update and should NEVER be shared with anyone. If you lose this key, you'll NOT be able to publish a new update to the current user base. It is crucial to store it in a safe place where you can always access it.

    To generate your keypair using the Tauri CLI, open a terminal and enter the following command:

    tauri signer generate -w ~/.tauri/myapp.key
    
  3. Tauri Configuration

    The Tauri bundler will automatically sign update artifacts if the TAURI_PRIVATE_KEY and TAURI_KEY_PASSWORD environment variables are set. TAURI_PRIVATE_KEY must be the string representation of your private key or a path pointing to your private key file. TAURI_KEY_PASSWORD must contain the private key's password if you configured one.

Window Customization

Tauri provides lots of options for customizing the look and feel of your app's window. You can create custom titlebars, have transparent windows, enforce size constraints, and more. This guide contains a number of quick examples for common cases.

Creating a Custom Titlebar

Apple Console App

To make the custom titlebar work, you need to disable decorations for the window by setting tauri.windows.decorations, WebviewWindow.setDecorations or WindowBuilder::decorations to false. Next, you need to add the HTML for the titlebar. Put this at the top of your <body> tag. Notice the data-tauri-drag-region data attribute, it allows you to drag the window around like a native titlebar would.

<div data-tauri-drag-region class="titlebar">
  <div class="titlebar-button" id="titlebar-minimize">
    <img
      src="https://api.iconify.design/mdi:window-minimize.svg"
      alt="minimize"
    />
  </div>
  <div class="titlebar-button" id="titlebar-maximize">
    <img
      src="https://api.iconify.design/mdi:window-maximize.svg"
      alt="maximize"
    />
  </div>
  <div class="titlebar-button" id="titlebar-close">
    <img src="https://api.iconify.design/mdi:close.svg" alt="close" />
  </div>
</div>

Now you need to add some CSS for the titlebar to keep it at the top of the screen and style the buttons. Note that you may need to move the rest of your content down so that the titlebar doesn't cover it.

.titlebar {
  height: 30px;
  background: #329ea3;
  user-select: none;
  display: flex;
  justify-content: flex-end;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
}
.titlebar-button {
  display: inline-flex;
  justify-content: center;
  align-items: center;
  width: 30px;
  height: 30px;
}
.titlebar-button:hover {
  background: #5bbec3;
}

Finally, you need some JavaScript to make the buttons work:

import { appWindow } from "@tauri-apps/api/window";
document
  .getElementById("titlebar-minimize")
  .addEventListener("click", () => appWindow.minimize());
document
  .getElementById("titlebar-maximize")
  .addEventListener("click", () => appWindow.toggleMaximize());
document
  .getElementById("titlebar-close")
  .addEventListener("click", () => appWindow.close());

Native Application Menu

Native application menus can be attached to individual windows or the whole application. Note that window-specific menus are only supported on Windows and Linux, but not on macOS.

Creating a Menu

To create a native window menu, import the Menu, Submenu, MenuItem and CustomMenuItem types. The MenuItem enum contains a collection of platform-specific items (currently not implemented on Windows). The CustomMenuItem allows you to create your own menu items and add special functionality to them.

use tauri::{CustomMenuItem, Menu, MenuItem, Submenu};

Create a Menu instance:

// here `"quit".to_string()` defines the menu item id,
// and the second parameter is the menu item label.
let quit = CustomMenuItem::new("quit".to_string(), "Quit");
let close = CustomMenuItem::new("close".to_string(), "Close");
let submenu = Submenu::new("File", Menu::new().add_item(quit).add_item(close));
let menu = Menu::new()
    .add_native_item(MenuItem::Copy)
    .add_item(CustomMenuItem::new("hide", "Hide"))
    .add_submenu(submenu);

Adding the Menu to all Windows

The defined menu can be set to all windows using the menu method of the tauri::Builder struct:

use tauri::{CustomMenuItem, Menu, MenuItem, Submenu};

fn main() {
  let menu = Menu::new(); // configure the menu
  tauri::Builder::default()
    .menu(menu)
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}

Adding the Menu to a Specific Window

You can create a window and set the menu to be used. This allows defining a specific menu set for each application window.

use tauri::{CustomMenuItem, Menu, MenuItem, Submenu};
use tauri::WindowBuilder;

fn main() {
  let menu = Menu::new(); // configure the menu
  tauri::Builder::default()
    .create_window(
      "main-window".to_string(),
      tauri::WindowUrl::App("index.html".into()),
      move |window_builder, webview_attributes| {
        (window_builder.menu(menu), webview_attributes)
      },
    )
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}

Listening to Events on Custom Menu Items

Each CustomMenuItem triggers an event when clicked. Use the on_menu_event callback to handle them, either on the global tauri::Builder or on an specific window.

Listening to Events on Global Menus

use tauri::{CustomMenuItem, Menu, MenuItem};

fn main() {
  let menu = vec![]; // insert the menu array here
  tauri::Builder::default()
    .menu(menu)
    .on_menu_event(|event| {
      match event.menu_item_id() {
        "quit" => {
          std::process::exit(0);
        }
        "close" => {
          event.window().close().unwrap();
        }
        _ => {}
      }
    })
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}

Listening to Events on Window Menus

use tauri::{CustomMenuItem, Menu, MenuItem};
use tauri::{Manager, WindowBuilder};

fn main() {
  let menu = vec![]; // insert the menu array here
  tauri::Builder::default()
    .create_window(
      "main-window".to_string(),
      tauri::WindowUrl::App("index.html".into()),
      move |window_builder, webview_attributes| {
        (window_builder.menu(menu), webview_attributes)
      },
    )
    .setup(|app| {
      let window = app.get_window("main-window").unwrap();
      let window_ = window.clone();
      window.on_menu_event(move |event| {
        match event.menu_item_id() {
          "quit" => {
            std::process::exit(0);
          }
          "close" => {
            window_.close().unwrap();
          }
          _ => {}
        }
      });
      Ok(())
    })
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}

Updating Menu Items

The Window struct has a menu_handle method, which allows updating menu items:

fn main() {
  tauri::Builder::default()
    .setup(|app| {
      let main_window = app.get_window("main").unwrap();
      let menu_handle = main_window.menu_handle();
      std::thread::spawn(move || {
        // you can also `set_selected`, `set_enabled` and `set_native_image` (macOS only).
        menu_handle.get_item("item_id").set_title("New title");
      })
      Ok(())
    })
}

System Tray

This guide walks you through adding a tray icon to the systems' notification area. Tray icons have their own context menu.

On macOS and Ubuntu, the Tray will be located on the top right corner of your screen, adjacent to your battery and wifi icons. On Windows, the Tray will usually be located in the bottom right corner.

Configuration

Add the following to your tauri.conf.json files tauri object:

  "tauri": {
+    "systemTray": {
+      "iconPath": "icons/icon.png",
+      "iconAsTemplate": true
+    }
  }

iconPath must point to a PNG file on macOS and Linux, and a .ico file must exist for Windows support. iconAsTemplate is a boolean value that determines whether the image represents a Template Image on macOS.

Creating a System Tray

To create a native system tray, import the SystemTray type:

use tauri::SystemTray;

and instantiate a new tray:

let tray = SystemTray::new();

Configuring a System Tray Context Menu

Optionally you can add a context menu that is visible when the tray icon is clicked. Import the SystemTrayMenu, SystemTrayMenuItem and CustomMenuItem types:

use tauri::{CustomMenuItem, SystemTrayMenu, SystemTrayMenuItem};

Create the SystemTrayMenu:

// here `"quit".to_string()` defines the menu item id,
// and the second parameter is the menu item label.
let quit = CustomMenuItem::new("quit".to_string(), "Quit");
let hide = CustomMenuItem::new("hide".to_string(), "Hide");

let tray_menu = SystemTrayMenu::new()
    .add_item(quit)
    .add_native_item(SystemTrayMenuItem::Separator)
    .add_item(hide);

Add the tray menu to the SystemTray instance:

let tray = SystemTray::new().with_menu(tray_menu);

Configure the App System Tray

The created SystemTray instance can be set using the system_tray API on the tauri::Builder struct:

use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu};

fn main() {
    let tray_menu = SystemTrayMenu::new(); // insert the menu items here
    let system_tray = SystemTray::new().with_menu(tray_menu);
    tauri::Builder::default()
        .system_tray(system_tray)
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Listening to System Tray Events

Each CustomMenuItem triggers an event when clicked. Also, Tauri emits tray icon click events. Use the on_system_tray_event callback to handle them:

use tauri::Manager;
use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu};

fn main() {
    let tray_menu = SystemTrayMenu::new(); // insert the menu items here
    tauri::Builder::default()
        .system_tray(SystemTray::new().with_menu(tray_menu))
        .on_system_tray_event(|app, event| match event {
            SystemTrayEvent::LeftClick {
                position: _,
                size: _,
                ..
            } => {
                println!("system tray received a left click");
            }
            SystemTrayEvent::RightClick {
                position: _,
                size: _,
                ..
            } => {
                println!("system tray received a right click");
            }
            SystemTrayEvent::DoubleClick {
                position: _,
                size: _,
                ..
            } => {
                println!("system tray received a double click");
            }
            SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
                "quit" => {
                    std::process::exit(0);
                }
                "hide" => {
                    let window = app.get_window("main").unwrap();
                    window.hide().unwrap();
                }
                _ => {}
            },
            _ => {}
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Updating the System Tray

The AppHandle struct has a tray_handle method, which returns a handle to the system tray, allowing updating tray icon and context menu items: Updating context menu items

use tauri::Manager;
use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu};

fn main() {
    let tray_menu = SystemTrayMenu::new(); // insert the menu items here
    tauri::Builder::default()
        .system_tray(SystemTray::new().with_menu(tray_menu))
        .on_system_tray_event(|app, event| match event {
            SystemTrayEvent::MenuItemClick { id, .. } => {
                // get a handle to the clicked menu item
                // note that `tray_handle` can be called anywhere,
                // just get a `AppHandle` instance
                // with `app.handle()` on the setup hook
                // and move it to another function or thread
                let item_handle = app.tray_handle().get_item(&id);
                match id.as_str() {
                    "hide" => {
                        let window = app.get_window("main").unwrap();
                        window.hide().unwrap();
                        // you can also `set_selected`, `set_enabled`
                        // and `set_native_image` (macOS only).
                        item_handle.set_title("Show").unwrap();
                    }
                    _ => {}
                }
            }
            _ => {}
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Updating the Tray Icon

Note that tauri::Icon must be a Path variant on Linux, and Raw variant on Windows and macOS.

app.tray_handle()
        .set_icon(tauri::Icon::Raw(include_bytes!("../path/to/myicon.ico")))
        .unwrap();

Sidecar (Embedding External Binaries)

Tauri allows you to embed external binaries, to save your users from installing additional dependencies (e.g., Node.js or Python).

Configuration

To add a Sidecar binary to your Tauri app, add the executables' absolute or relative path to the tauri.bundle.externalBin array. The Tauri CLI will bundle all sidecars into the final package.

{
  "tauri": {
    "bundle": {
      "externalBin": [
+        "/absolute/path/to/app",
+        "relative/path/to/binary",
+        "bin/python"
      ]
    }
  }
}
Listing 4-TODO:

When bundling the final application, the Tauri bundlers appends the [target triple] to the specified path, so for example, with the following configuration "externalBin": ["bin/python"] Tauri will attempt to bundle the following file src-tauri/bin/python-x86_64-unknown-linux-gnu on x86 Linux and src-tauri/bin/python-aarch64-apple-darwin on Apple silicon macOS.

To find your current platforms' target triple, open a terminal and enter the following command:

rustc -Vv | grep host

Here's a Node.js script to append the target triple to a binary:

const execa = require("execa");
const fs = require("fs");

let extension = "";
if (process.platform === "win32") {
  extension = ".exe";
}

async function main() {
  const rustInfo = (await execa("rustc", ["-vV"])).stdout;
  const targetTriple = /host: (\S+)/g.exec(rustInfo)[1];
  if (!targetTriple) {
    console.error("Failed to determine platform target triple");
  }
  fs.renameSync(
    `src-tauri/binaries/app${extension}`,
    `src-tauri/binaries/app-${targetTriple}${extension}`
  );
}

main().catch((e) => {
  throw e;
});

Running the Sidecar Binary

Tauri takes care of bundling the Sidecar binary, but you are in charge of actually running it. This means you are also in charge of killing the child process when your app closes; otherwise, you pollute the users' machine with orphan processes.

Rust

The tauri::api::process::Command struct provides a convenient constructor for Sidecar binaries, Command::new_sidecar, that will take care of appending the correct target triple to match the filename you had to assign previously.

let (mut rx, mut child) = Command::new_sidecar("my-sidecar")
  .expect("failed to create `my-sidecar` binary command")
  .spawn()
  .expect("Failed to spawn sidecar");

tauri::async_runtime::spawn(async move {
  // read events such as stdout
  while let Some(event) = rx.recv().await {
    if let CommandEvent::Stdout(line) = event {
      window
        .emit("message", Some(format!("'{}'", line)))
        .expect("failed to emit event");
      // write to stdin
      child.write("message from Rust\n".as_bytes()).unwrap();
    }
  }
});

JavaScript

In Frontend JavaScript code, you may use the Command.sidecar static method that, in-turn takes care of appending the right target triple.

import { Command } from "@tauri-apps/api/shell";
// alternatively, use `window.__TAURI__.shell.Command`
// `my-sidecar` is the value specified on `tauri.conf.json > tauri > bundle > externalBin`
const command = Command.sidecar("my-sidecar");
const output = await command.execute();

Using Node.js as a Sidecar

The sidecar feature can only bundle self-contained binaries, making Node.js applications difficult to bundle. You can either include a stock Node.js binary as a Sidecar and your JavaScript file as a Resource, or compile the Node.js runtime and code into a standalone binary using pkg. The official Tauri sidecar example demonstrates the latter technique.

Splashscreen

In case your Frontend is rather heavy and takes some time to load, or you need to perform some initialization procedures in Rust before displaying your main window, a splash screen can improve the loading experience for the user.

Configuration

First, create a splashscreen.html in your distDir that contains the HTML code for a splashscreen. Then, add the following to your tauri.conf.json:

"windows": [
  {
    "title": "Tauri App",
    "width": 800,
    "height": 600,
    "resizable": true,
    "fullscreen": false,
+   "visible": false // Hide the main window by default
  },
  // Add the splashscreen window
+ {
+   "width": 400,
+   "height": 200,
+   "decorations": false,
+   "url": "splashscreen.html",
+   "label": "splashscreen"
+ }
]

Your main window will be hidden, and the splashscreen window will show when your app is launched. Next, you'll need a way to close the splashscreen and show the main window when your app is ready. How you do this depends on what you are waiting for before closing the splashscreen.

Waiting for the Frontend

If you are waiting for your web code, you'll want to create a close_splashscreen command.

use tauri::Manager;
// Create the command:
// This command must be async so that it doesn't run on the main thread.
#[tauri::command]
async fn close_splashscreen(window: tauri::Window) {
  // Close splashscreen
  if let Some(splashscreen) = window.get_window("splashscreen") {
    splashscreen.close().unwrap();
  }
  // Show main window
  window.get_window("main").unwrap().show().unwrap();
}

// Register the command:
fn main() {
  tauri::Builder::default()
    // Add this line
    .invoke_handler(tauri::generate_handler![close_splashscreen])
    .run(tauri::generate_context!())
    .expect("failed to run app");
}

Then, you can call it from your Frontend:

// With the Tauri API npm package:
import { invoke } from "@tauri-apps/api/tauri";
// With the Tauri global script:
const invoke = window.__TAURI__.invoke;

document.addEventListener("DOMContentLoaded", () => {
  // This will wait for the window to load, but you could
  // run this function on whatever trigger you want
  invoke("close_splashscreen");
});

Waiting for Rust

If you are waiting for Rust code to run, put it in the setup callback so you have access to the App instance:

use tauri::Manager;
fn main() {
  tauri::Builder::default()
    .setup(|app| {
      let splashscreen_window = app.get_window("splashscreen").unwrap();
      let main_window = app.get_window("main").unwrap();
      // we perform the initialization code on a new task so the app doesn't freeze
      tauri::async_runtime::spawn(async move {
        // initialize your app here instead of sleeping :)
        println!("Initializing...");
        std::thread::sleep(std::time::Duration::from_secs(2));
        println!("Done initializing.");

        // After it's done, close the splashscreen and display the main window
        splashscreen_window.close().unwrap();
        main_window.show().unwrap();
      });
      Ok(())
    })
    .run(tauri::generate_context!())
    .expect("failed to run app");
}

Icons

Tauri ships with a default iconset based on its logo. This is probably NOT what you want when you ship your application. To help with this common situation, Tauri provides the tauricon command that will take an input file ("./app-icon.png" by default) and create all the icons needed for the various platforms:

Installation

You can install the tauricon package either locally as a dev dependency:

npm

npm install -D github:tauri-apps/tauricon

yarn

yarn add -D github:tauri-apps/tauricon

pnpm

pnpm add -D github:tauri-apps/tauricon

or globally:

npm

npm install -g github:tauri-apps/tauricon

yarn

yarn add -g github:tauri-apps/tauricon

pnpm

pnpm add -g github:tauri-apps/tauricon

If you decide to use tauricon as a local package with npm (not yarn), you need to add a custom script to your package.json: package.json

{
  "scripts": {
+    "tauricon": "tauricon"
  }
}

Usage

npm tauricon --help

Description
  Create all the icons you need for your Tauri app.
  The icon path is the source icon (png, 1240x1240 with transparency), it defaults
  to './app-icon.png'.

Usage
  tauricon [ICON-PATH]

Options
  --help, -h          Displays this message
  --log, l            Logging [boolean]
  --target, t         Target folder (default: 'src-tauri/icons')
  --compression, c    Compression type [optipng|zopfli]
  --ci                Runs the script in CI mode

Created icons will be placed in your src-tauri/icons folder, where they will automatically be included in your built app.

If you need to source your icons from some other location, you can edit this part of the src-tauri/tauri.conf.json file:

{
  "tauri": {
    "bundle": {
      "icon": [
-        "icons/32x32.png",
-        "icons/128x128.png",
-        "icons/128x128@2x.png",
-        "icons/icon.icns",
-        "icons/icon.ico"
+        "otherpath/icons/32x32.png",
+        "otherpath/icons/128x128.png",
+        "otherpath/icons/128x128@2x.png",
+        "otherpath/icons/icon.icns",
+        "otherpath/icons/icon.ico"
      ]
    }
  }
}

Note on filetypes:

  • .icns is used for macOS builds
  • .ico is used for Windows builds
  • .png is used for Linux builds

Command Line Interface

With Tauri you can give your app a Command Line Interface (CLI) through clap, a robust command-line argument parser written in Rust. With a simple CLI configuration in your tauri.conf.json file, you can define your interface and read its argument matches map on JavaScript and/or Rust.

Base Configuration

Under tauri.conf.json, you have the following structure to configure the interface:

Filename: src-tauri/tauri.conf.json

{
  "tauri": {
    "cli": {
      // command description that's shown on help
      "description": "",
      // command long description that's shown on help
      "longDescription": "",
      // content to show before the help text
      "beforeHelp": "",
      // content to show after the help text
      "afterHelp": "",
      // list of arguments of the command, we'll explain it later
      "args": [],
      "subcommands": {
        "subcommand-name": {
          // configures a subcommand that is accessible
          // with `./app subcommand-name --arg1 --arg2 --etc`
          // configuration as above, with "description", "args", etc.
        }
      }
    }
  }
}

Note: All JSON configurations here are just samples, many other fields have been omitted for the sake of clarity.

Adding Arguments

The args array represents the list of arguments accepted by its command or subcommand. You can find more details about the way to configure them here.

Positional Arguments

A positional argument is identified by its position in the list of arguments. With the following configuration:

Filename: src-tauri/tauri.conf.json

{
  "args": [
    {
      "name": "source",
      "index": 1,
      "takesValue": true
    },
    {
      "name": "destination",
      "index": 2,
      "takesValue": true
    }
  ]
}

Users can run your app as ./app tauri.txt dest.txt and the arg matches map will define source as "tauri.txt" and destination as "dest.txt".

Named Arguments

A named argument is a (key, value) pair where the key identifies the value. With the following configuration:

Filename: src-tauri/tauri.conf.json

{
  "args": [
    {
      "name": "type",
      "short": "t",
      "takesValue": true,
      "multiple": true,
      "possibleValues": ["foo", "bar"]
    }
  ]
}

Users can run your app as ./app --type foo bar, ./app -t foo -t bar or ./app --type=foo,bar and the arg matches map will define type as ["foo", "bar"].

Flag Arguments

A flag argument is a standalone key whose presence or absence provides information to your application. With the following configuration:

Filename: src-tauri/tauri.conf.json

{
  "args": [
    "name": "verbose",
    "short": "v",
    "multipleOccurrences": true
  ]
}

Users can run your app as ./app -v -v -v, ./app --verbose --verbose --verbose or ./app -vvv and the arg matches map will define verbose as true, with occurrences = 3.

Subcommands

Some CLI applications has additional interfaces as subcommands. For instance, the git CLI has git branch, git commit and git push. You can define additional nested interfaces with the subcommands array:

Filename: src-tauri/tauri.conf.json

{
  "cli": {
    ...
    "subcommands": {
      "branch": {
        "args": []
      },
      "push": {
        "args": []
      }
    }
  }
}

Its configuration is the same as the root application configuration, with the description, longDescription, args, etc.

Reading the matches

Rust

use tauri::api::cli::get_matches;

fn main() {
    let context = tauri::generate_context!();
    let cli_config = context.config().tauri.cli.clone().unwrap();

    match get_matches(&cli_config) {
        // `matches` here is a Struct with { args, subcommand }.
        // `args` is `HashMap<String, ArgData>`
        // where `ArgData` is a struct with { value, occurances }.
        // `subcommand` is `Option<Box<SubcommandMatches>>`
        // where `SubcommandMatches` is a struct with { name, matches }.
        Ok(matches) => {
            println!("{:?}", matches)
        }
        Err(_) => {}
    };

    tauri::Builder::default()
        .run(context)
        .expect("error while running tauri application");
}

JavaScript

import { getMatches } from "@tauri-apps/api/cli";

getMatches().then((matches) => {
  // do something with the { args, subcommand } matches
});

Complete documentation

You can find more about the CLI configuration here.

Appendix C: Version Tables

macOS

macOSsafariwebkitnote
12.315.417613
12.215.317612
12.115.2
12.015.0
11.314.1611This one is a guess because of the same release date
11.014.0610
10.1513.x608
10.1412.x606/60710.14.4 upgraded safari to 12.1 - No idea if that's true for the system webview too
10.1311.x604/60510.13.5 upgraded safari to 11.1 - No idea if that's true for the system webview too

D - JSON Schemas

Dynamic JSON Format

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "additionalProperties": false,
  "required": ["url", "version"],
  "properties": {
    "url": { "type": "string" },
    "version": { "type": "string" },
    "pub_date": { "type": "string" },
    "notes": { "type": "string" },
    "signature": { "type": "string" }
  }
}
Listing D-1: Formal schema for the updater's dynamic JSON format.

Static JSON Format

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "additionalProperties": false,
  "required": ["version"],
  "properties": {
    "version": { "type": "string" },
    "pub_date": { "type": "string" },
    "notes": { "type": "string" },
    "platforms": {
      "type": "object",
      "additionalProperties": false,
      "patternProperties": {
        "^(linux|windows|darwin)-(x86_64|i686|aarch64|armv7)$": {
          "type": "object",
          "required": ["url"],
          "properties": {
            "url": { "type": "string" },
            "signature": { "type": "string" },
            "with_elevated_task": { "type": "boolean" }
          }
        }
      }
    }
  }
}
Listing D-2: Formal schema for the updater's static JSON format.