Tauri: wrap and package¶
Exercise tauri-wrap-and-package-annex.pdf · PDF
Your app is finished and documented. Your cousin wants to try it. What do you send? The repo link? Then your cousin has to install Node.js, npm and your dependencies before npm run dev does anything. And your grandmother?
A user wants one file: double-click, Next, Next, Finish, an icon in the menu. Until now your app has only run on a developer's machine. An app nobody can install is a prototype with good manners. This page gets it out of the workshop: you put your SvelteKit app in its own window, decide what travels with it, give it an icon and build your first package.
What you will learn
- Explain what a Tauri app is: a web frontend drawn by the system WebView, plus a small native core in Rust
- Wrap a SvelteKit app in six steps, with
adapter-staticandssr = false - Read
tauri.conf.jsonand say what identifier, version, frontendDist and bundle are for - Decide what must travel in the installer, and declare extra files in bundle > resources
- Generate every icon size from one image with
tauri icon - Build a package with
tauri buildand find it on disk - The trap: why "it works in
tauri dev" does not mean it works installed
The suitcase¶
Keep one picture in your head for the whole page: packaging is packing a suitcase.
You are travelling to a house you have never seen. What you need and the house does not have goes in the suitcase. What the house already has, like a bed and running water, stays at home. Pack too little and you arrive without shoes. Pack the whole wardrobe and you pay for 100 kg.
Your app is the traveller. The user's computer is the house.
What Tauri is¶
Tauri is a tool that puts your web frontend in a native window. The window is drawn by the WebView the operating system already has, and a small program written in Rust opens it and talks to the system.
A WebView is a browser engine without the browser: no address bar, no tabs, only the part that draws HTML and CSS and runs JavaScript. Rust is a compiled programming language. You will read a few lines of it, and you do not need to learn it to package an app.
Tauri 2.0 came out on 2 October 2024, and it is the major version you use here. The official docs say that "a minimal Tauri app can be less than 600KB". That is possible because it does not carry a browser. The house already has the bed:
| System | The WebView already there | Engine family |
|---|---|---|
| Windows | WebView2 | Chromium, the engine of Edge and Chrome |
| macOS | WKWebView | WebKit, the engine of Safari |
| Linux | WebKitGTK | WebKit |
flowchart TB
subgraph APP ["What you ship"]
F["<b>Frontend</b><br>build/: HTML, CSS, JS<br>the clothes"]
R["<b>Rust core</b><br>src-tauri/: opens the window, can touch files<br>the traveller's hands"]
F <-->|"invoke(): JavaScript calls<br>a Rust function by name"| R
end
subgraph OS ["What the house already has"]
W["<b>System WebView</b><br>draws your frontend<br>the bed"]
end
W -->|"draws"| F
The line between the two halves is a bridge. In Rust you mark a function as a command. In JavaScript you call it by name and wait for the answer:
#[tauri::command]
fn my_custom_command() {
println!("I was invoked from JavaScript!");
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![my_custom_command])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
The other way: Electron¶
Electron, the tool behind VS Code and Discord, packs its own copy of Chromium in every app. The bed travels in the suitcase.
| Tauri | Electron | |
|---|---|---|
| Browser engine | The system's WebView | Its own Chromium, inside the app |
| Weight | Light | Heavy |
| Your CSS meets | Three different engines | The same engine everywhere |
Tauri's price is on the last row. If you only tested your app in Chrome, some things will look different in the Tauri window on Linux: a font, a scrollbar, a date input. Look for those differences on purpose.
Careful: Tauri does not turn Svelte into a native app
Your Svelte code is still HTML, CSS and JavaScript, drawn by a browser engine. What is native is the window around it and the Rust program behind it.
Before you start: the toolchain¶
Building a Tauri app needs Rust and some system libraries. Your classroom PC already has them. To work at home, install them once.
Once a project is wrapped, npm run tauri info prints a report of what your machine has and what is missing.
Wrapping a SvelteKit app in six steps¶
Do this on a new git branch, for example tauri, in your project from Unit 3 and later. The example on this page is NoteBox, a tiny notes app.
1. Install the tools.
2. Export static files. There is no server inside a window, so SvelteKit must produce plain files. An adapter is the SvelteKit plugin that shapes the build for the place it will run, and adapter-static writes a folder of plain files called build/. Find where your project says adapter-auto and replace it. Where it says so depends on how old your project template is:
Keep any other options your file already has. The fallback is the one HTML page that loads your app and then shows the right route. Without it, the window has nothing to open.
3. Render in the WebView, not on a server. Create src/routes/+layout.js with one line:
4. Add a shortcut for the command-line tool. In package.json, under scripts:
5. Create the native half.
It asks six questions:
| Question | Answer |
|---|---|
| App name | Your app's name |
| Window title | What the title bar shows |
| Frontend dist (where the built web files are) | ../build |
| Dev server URL | http://localhost:5173 |
| Frontend dev command | npm run dev |
| Frontend build command | npm run build |
6. Run it in its own window.
The first run compiles about 400 Rust packages, called crates. Expect several minutes. Do something else while it runs. Then a window appears, with no address bar, no tabs and its own entry in the taskbar. It is your app.
Careful: ssr = false does not make your app worse
SSR (server-side rendering) exists so a server can send ready-made HTML. A desktop window has no server: the files are already on the disk. Turning SSR off is not a shortcut. It is the truth about where the app runs.
When it goes wrong¶
| What you see | Why | Fix |
|---|---|---|
The build fails or the page is empty, and you have a +page.server.js, a form action or a +server.js route |
Those need a server, and a static export has none | Move the data loading to +page.js, which runs in the WebView, or to a fetch inside the component |
| The port is already in use | A forgotten npm run dev in another terminal |
Close it. Tauri starts its own |
tauri: not found |
Step 4 is missing, or npm install did not run after step 1 |
Do those steps |
| A white window | frontendDist is not ../build, or the fallback is missing |
Check both |
| Camera or microphone features from Unit 5 do nothing | WebKitGTK does not offer everything Chrome does. It has no voice recognition from the Web Speech API, and the camera may need extra work | Write it in your README as a known limitation, and keep those features web only |
The label on the suitcase¶
tauri init created a new folder. Four things in it matter for now:
In src-tauri/ |
What it is |
|---|---|
tauri.conf.json |
The label on the suitcase: name, version, identifier, icons, what to pack |
src/lib.rs |
The Rust program, about ten lines |
icons/ |
The app's icons |
Cargo.toml |
The package.json of the Rust side |
Open tauri.conf.json and read the label:
| Key | What it is |
|---|---|
productName |
The name the user sees: window, menu entry, package file name |
version |
Goes in the package name. The updater uses it to decide what is newer |
identifier |
A unique id for the app in reverse-domain form |
build.frontendDist |
Where the built frontend is: ../build for SvelteKit |
build.devUrl |
The dev server Tauri opens in tauri dev: http://localhost:5173 |
bundle.targets |
Which packages to make: "all" or a list of formats |
bundle.icon |
The icon files |
bundle.resources |
Extra files that must travel with the app |
Change two of them before you build:
- Set
identifierto something of yours, such ascom.iessonferrer.notebox. A reverse domain is a web domain written backwards. It can only contain letters, digits, hyphens and dots. Leave the defaultcom.tauri.devandtauri buildrefuses to start. - Set
versionto0.1.0. It ends up in the file name of the package.
Careful: the identifier is not just a name
It names the folder where the user's data lives. Change it in a later version and the user's notes seem to disappear, because the app now looks in a different folder.
What goes in the box?¶
Your project folder is huge. The installer is tiny. So what travels?
| In the installer | Not in the installer |
|---|---|
build/: your HTML, CSS and JS |
node_modules/ |
| The compiled Rust program | Your source files, such as src/routes/+page.svelte |
| The icons | Node.js and npm |
Files you declare, such as resources/welcome-notes.json |
Rust and cargo |
| A licence text file, if you declare one | Storybook stories |
| WebView2 itself | |
| The user's own notes |
node_modules is the kitchen. The installer carries the dish.
Two rows deserve a second look:
- WebView2. By default, the Windows installer does not carry it. If it is missing, the installer downloads a small program, the bootstrapper, that installs it. Carrying the full offline installer inside your package adds about 127 MB.
- The user's notes. They do not exist yet when you install. They are created later, on the user's disk. You will meet them again when you uninstall.
The missing file¶
NoteBox reads its welcome notes from src-tauri/resources/welcome-notes.json, through a Rust command. In tauri dev it works. Then you run the release program from another folder, and a yellow panel says "Welcome notes not found".
What changed? In dev, the app reads from your project folder, where every file is at hand. A packaged app only has what was packed. You arrived without shoes.
The fix is one line of the label, in bundle > resources:
"bundle": {
"active": true,
"targets": "all",
"resources": ["resources/welcome-notes.json"],
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"]
}
Paths are relative to src-tauri/. Patterns work too: "resources/**/*" takes every file in that folder and its subfolders.
In the installed app the file lands in the resource folder: /usr/lib/NoteBox/resources/ in our .deb, and next to the .exe on Windows. The path changes per system, so your Rust code never writes it by hand. It asks Tauri instead:
Careful: \"it works in tauri dev\" proves nothing about the package
tauri dev runs inside your project. The only honest test is the installed package, on a machine or a user account that has never seen the project.
The app icon¶
A package with the default Tauri icon says "unfinished" before the app even opens. So design one in Figma, in a 1024 × 1024 frame.
- One shape, and two or three flat colours.
- No text.
- Transparent outside the tile.
- Keep the subject inside a safe area, away from the edges.
- Look at it at 128, 32 and 16 px. If your neighbour cannot say what it is at 16 px, remove detail. Do not add it.
Export the frame as PNG at 1x and save it as app-icon.png in the project root. Then run one command:
It reads ./app-icon.png, a square PNG or SVG with transparency, and writes the whole set to src-tauri/icons/:
| File | For |
|---|---|
32x32.png, 128x128.png, 128x128@2x.png, icon.png |
Linux |
icon.icns |
macOS |
icon.ico |
Windows: layers of 16, 24, 32, 48, 64 and 256 px in one file |
There is nothing to edit afterwards: bundle > icon already lists these files.
Your first package¶
This one command does three things:
flowchart LR
A["<b>npm run build</b><br>the frontend<br>into build/"] --> B["<b>Rust compile</b><br>release mode<br>the program"]
B --> C["<b>Bundler</b><br>packs everything for<br>the current system"]
C --> D["<b>Packages</b><br>src-tauri/target/<br>release/bundle/"]
The bundler is the part of Tauri that makes the packages. On Ubuntu it makes a .deb, an .rpm and an .AppImage. To go faster, ask for one:
The extra -- tells npm that what follows belongs to Tauri, not to npm.
Release mode is a different build from dev, so everything compiles again. Expect several more minutes. When it ends, the last lines print the path:
| On Ubuntu | On Windows | |
|---|---|---|
| Option | --bundles deb, also rpm and appimage |
--bundles nsis, also msi |
| Result | src-tauri/target/release/bundle/deb/NoteBox_0.1.0_amd64.deb |
src-tauri\target\release\bundle\nsis\NoteBox_0.1.0_x64-setup.exe |
NoteBox's .deb weighs 2.9 MB. Try it on your own computer: double-click the file, or run sudo apt install ./NoteBox_0.1.0_amd64.deb with your own file name.
Careful: one build does not give you every system
tauri build packages for the system it runs on. A build on Ubuntu gives you no Windows installer. To reach the others, you use a robot.
Key takeaways¶
- Tauri puts your web frontend in a native window. The system WebView draws it and a small Rust core opens it, so no browser travels in the package.
- Wrapping a SvelteKit app takes
adapter-staticwith a fallback,ssr = false, thentauri initandtauri dev. Server-only code cannot come along. tauri.conf.jsonis the label on the suitcase. The identifier must be your own, in reverse-domain form, and it names the user's data folder.node_modulesis the kitchen and the installer carries the dish. Onlybuild/, the Rust program, the icons and what you list inbundle > resourcestravel.tauri iconmakes every icon size from one square image.tauri buildmakes packages for the current system only. Test the installed package, nottauri dev.
Quick check¶
1. What draws the interface of a Tauri app on the user's computer?
- A copy of Chromium packed inside the app
- The WebView the operating system already has
- The Rust core, which converts Svelte into native controls
- A small web server installed with the app
Tauri uses the system WebView: WebView2 on Windows, WKWebView on macOS and WebKitGTK on Linux. Carrying Chromium is what Electron does. Your Svelte code stays HTML, CSS and JavaScript, and the Rust core only opens the window and talks to the system.
2. Why does a Tauri-wrapped SvelteKit app set ssr = false?
- Because SSR makes the Rust compilation much slower
- Because the WebView cannot run any JavaScript from SvelteKit
- Because the window has no server to render pages on
- Because static files are not allowed inside a package
SSR lets a server send ready-made HTML. In a desktop window the files are already on the disk and no server exists, so the pages render in the WebView.
3. Your project has a +server.js route. What happens when you export it with adapter-static?
- Tauri converts the route into a Rust command by itself
- It cannot work, because a static export has no server to run it
- It keeps working, but only on Linux
- The installer adds a copy of Node.js to run it
adapter-static only produces plain files. +page.server.js, form actions and +server.js all need a server. Move the data loading to +page.js or to a fetch in the component.
4. Which of these travels inside the installer of your app?
- The
node_modulesfolder - The Node.js runtime
- The file
src/routes/+page.svelte - The compiled files in
build/
node_modules and your sources are the kitchen: they are needed to cook, not to eat. After npm run build, the result lives in build/, and that is what the package carries, along with the Rust program, the icons and your resources.
5. An app reads src-tauri/resources/prices.csv through a Rust command. It works in tauri dev, but the installed version says the file is missing. What do you change?
- Add the file to
bundle > resourcesintauri.conf.json - Change
build.devUrlso it points to the file - Run
npm run tauri devon the user's computer - Rename the file to match the
identifier
This is the same bug as the welcome notes. In dev the app reads from your project folder, and a package only has what was packed. bundle > resources declares extra files, with paths relative to src-tauri/.
6. You run tauri build and it refuses to start, complaining about com.tauri.dev. Which key do you edit?
-
productName -
version -
identifier -
bundle.targets
com.tauri.dev is the default identifier, and tauri build rejects it. Write your own in reverse-domain form, using only letters, digits, hyphens and dots.
7. A classmate builds on Ubuntu and wants a Windows installer for a friend. What result should they expect from npm run tauri build?
- A
.exeand a.deb, because the bundler covers every system - A
.exeonly, if the WebView2 runtime is downloaded first - Linux packages only, because it packages for the current system
- No packages at all, because Tauri needs Windows to build
The bundler works for the system it runs on. On Ubuntu that means .deb, .rpm and .AppImage. A Windows installer needs a Windows machine, and the usual way to get one is a CI service.
You have a package now, but its installer still looks like nobody's. Next you design what the user sees between the double-click and the first screen: Installers and the wizard.