code2app 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +111 -0
  2. package/assets/icon-source.png +0 -0
  3. package/bin/cli.js +248 -0
  4. package/dist/index.html +24 -0
  5. package/docs/APP_STORES.md +129 -0
  6. package/docs/BUILDING.md +134 -0
  7. package/docs/LOCAL_APPS.md +116 -0
  8. package/docs/MOBILE.md +78 -0
  9. package/docs/OAUTH.md +76 -0
  10. package/package.json +66 -0
  11. package/profiles/README.md +56 -0
  12. package/profiles/example.json +37 -0
  13. package/scripts/build-sidecar.mjs +80 -0
  14. package/scripts/configure.mjs +222 -0
  15. package/scripts/generate-icons.mjs +53 -0
  16. package/scripts/generate-placeholder-icons.mjs +186 -0
  17. package/scripts/lib/png.mjs +141 -0
  18. package/scripts/profile.mjs +147 -0
  19. package/src-tauri/Cargo.lock +5832 -0
  20. package/src-tauri/Cargo.toml +47 -0
  21. package/src-tauri/build.rs +3 -0
  22. package/src-tauri/capabilities/default.json +15 -0
  23. package/src-tauri/capabilities/remote.json +17 -0
  24. package/src-tauri/icons/128x128.png +0 -0
  25. package/src-tauri/icons/128x128@2x.png +0 -0
  26. package/src-tauri/icons/32x32.png +0 -0
  27. package/src-tauri/icons/64x64.png +0 -0
  28. package/src-tauri/icons/Square107x107Logo.png +0 -0
  29. package/src-tauri/icons/Square142x142Logo.png +0 -0
  30. package/src-tauri/icons/Square150x150Logo.png +0 -0
  31. package/src-tauri/icons/Square284x284Logo.png +0 -0
  32. package/src-tauri/icons/Square30x30Logo.png +0 -0
  33. package/src-tauri/icons/Square310x310Logo.png +0 -0
  34. package/src-tauri/icons/Square44x44Logo.png +0 -0
  35. package/src-tauri/icons/Square71x71Logo.png +0 -0
  36. package/src-tauri/icons/Square89x89Logo.png +0 -0
  37. package/src-tauri/icons/StoreLogo.png +0 -0
  38. package/src-tauri/icons/icon.icns +0 -0
  39. package/src-tauri/icons/icon.ico +0 -0
  40. package/src-tauri/icons/icon.png +0 -0
  41. package/src-tauri/src/generated_config.rs +22 -0
  42. package/src-tauri/src/lib.rs +212 -0
  43. package/src-tauri/src/main.rs +6 -0
  44. package/src-tauri/tauri.conf.json +84 -0
  45. package/vitest.config.mjs +35 -0
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ <!-- template-git-repo:badges:start -->
2
+ <p align="center">
3
+ <a href="https://starterdocs.vtempest.workers.dev/docs/packages/native-app-wrapper"><img src="https://img.shields.io/badge/Docs-blue?logo=ReadTheDocs&logoColor=white" alt="Documentation" /></a>
4
+ <br />
5
+ <a href="https://github.com/OpenSourceAGI/dev-tools-starter-agent/stargazers"><img src="https://img.shields.io/github/stars/OpenSourceAGI/dev-tools-starter-agent" alt="GitHub Stars" /></a>
6
+ <br />
7
+ <a href="https://github.com/OpenSourceAGI/dev-tools-starter-agent/issues"><img src="https://img.shields.io/github/issues/OpenSourceAGI/dev-tools-starter-agent?logo=github" alt="GitHub Issues" /></a>
8
+ <a href="https://github.com/OpenSourceAGI/dev-tools-starter-agent/pulls"><img src="https://img.shields.io/github/issues-pr/OpenSourceAGI/dev-tools-starter-agent?logo=github&label=PRs" alt="Open Pull Requests" /></a>
9
+ <a href="https://github.com/OpenSourceAGI/dev-tools-starter-agent/pulls?q=is%3Apr+is%3Aclosed"><img src="https://img.shields.io/github/issues-pr-closed/OpenSourceAGI/dev-tools-starter-agent?logo=github&label=PRs%20merged&color=8957e5" alt="Merged Pull Requests" /></a>
10
+ <a href="https://github.com/OpenSourceAGI/dev-tools-starter-agent/discussions"><img src="https://img.shields.io/github/discussions/OpenSourceAGI/dev-tools-starter-agent" alt="GitHub Discussions" /></a>
11
+ <a href="https://github.com/OpenSourceAGI/dev-tools-starter-agent/commits/master/"><img src="https://img.shields.io/github/last-commit/OpenSourceAGI/dev-tools-starter-agent.svg" alt="GitHub last commit" /></a>
12
+ <br />
13
+ <img src="https://img.shields.io/badge/Bun-14151A?logo=bun&logoColor=white" alt="Bun" /> <img src="https://img.shields.io/badge/Tauri-24C8D8?logo=tauri&logoColor=white" alt="Tauri" /> <img src="https://img.shields.io/badge/Vitest-6E9F18?logo=vitest&logoColor=white" alt="Vitest" />
14
+ </p>
15
+ <!-- template-git-repo:badges:end -->
16
+
17
+ <!-- skills:install:start -->
18
+ **🤖 Agent skill** — `npx skills@latest add https://github.com/OpenSourceAGI/dev-tools-starter-agent --skill native-app-wrapper` ([what it covers](../../skills/native-app-wrapper/SKILL.md))
19
+ <!-- skills:install:end -->
20
+
21
+ # native-app-wrapper
22
+
23
+ A [Tauri](https://tauri.app) shell that turns one JSON profile into a native desktop
24
+ (Windows/macOS/Linux) and mobile (Android/iOS) app. It packages one of two things:
25
+
26
+ - **A website** (`"mode": "remote"`) — a titled window that loads the site directly, plus the
27
+ OS-level plumbing a plain webview doesn't get for free: a real app icon on every platform, a
28
+ Google-OAuth-compatible login handoff, and a fullscreen toggle.
29
+ - **A command-line tool** (`"mode": "local"`) — a bundled HTML frontend backed by the CLI itself,
30
+ shipped alongside the app binary as a [Tauri sidecar](https://v2.tauri.app/develop/sidecar/), so
31
+ the app runs with no Node, Python, or other runtime installed on the user's machine. See
32
+ [`docs/LOCAL_APPS.md`](docs/LOCAL_APPS.md).
33
+
34
+ `packages/about-system-info/native` is a working example of the second kind — the `about-system`
35
+ CLI shipped as an installable desktop app for all three desktop OSes.
36
+
37
+ ## What this is not
38
+
39
+ Not a place for app-specific UI or logic. In remote mode the wrapped site provides all of that; in
40
+ local mode the app's own `dist/` and its sidecar do. This package's job stops at: open a window,
41
+ register the app's identity (icon, name, bundle id) with each OS, hand a browser-based OAuth login
42
+ back to that window, and expose the bundled CLI's output to the bundled frontend. Anything more
43
+ belongs in the app being wrapped.
44
+
45
+ ## Quick start
46
+
47
+ Scaffold a copy of the wrapper for your own app:
48
+
49
+ ```bash
50
+ cd packages/native-app-wrapper
51
+ node bin/cli.js init ../../apps/my-app/native --profile-file ./my-app.json
52
+ cd ../../apps/my-app/native
53
+ npm install
54
+ npm run dev
55
+ ```
56
+
57
+ `init` copies the wrapper's sources into the target directory, gives that copy your profile as its
58
+ only identity, regenerates the Tauri config and Rust constants from it, renames the crate, and
59
+ draws a placeholder icon set so the copy builds immediately. It also leaves out what the profile
60
+ doesn't use: a desktop-only profile gets no `android:build` script, an offline CLI app gets no
61
+ guide to browser OAuth handoffs. Nothing links back to this package afterward — the copy is a
62
+ standalone app you can commit next to the thing it wraps, and updating it later means copying the
63
+ changed scripts across, not re-running `init` over your own `dist/`.
64
+
65
+ To work on this package itself instead, `npm run dev` here opens a window on the untouched
66
+ `profiles/example.json` (`https://example.com`), which is what the checked-in
67
+ `src-tauri/tauri.conf.json` is generated from.
68
+
69
+ ## Layout
70
+
71
+ ```
72
+ native-app-wrapper/
73
+ ├── bin/cli.js # init / configure / icons
74
+ ├── profiles/ # one JSON file = one app identity (see profiles/README.md)
75
+ │ └── example.json # the template, and this package's own default
76
+ ├── scripts/
77
+ │ ├── profile.mjs # loads + validates a profile; one definition of the schema
78
+ │ ├── configure.mjs # profile -> tauri.conf.json, generated_config.rs, remote.json
79
+ │ ├── build-sidecar.mjs # profile's sidecar.build -> src-tauri/binaries/<name>-<triple>
80
+ │ ├── generate-icons.mjs # profile's iconSource -> full src-tauri/icons/ set (Tauri CLI)
81
+ │ ├── generate-placeholder-icons.mjs # draws a buildable icon set with no artwork and no deps
82
+ │ └── lib/png.mjs # the PNG/ICO/ICNS writers that make the above possible
83
+ ├── src-tauri/
84
+ │ ├── src/lib.rs # window setup, deep-link OAuth handoff, sidecar bridge, fullscreen toggle
85
+ │ ├── capabilities/ # Tauri's permission grants (default.json + a generated remote.json)
86
+ │ ├── icons/ # generated; checked in so a fresh clone builds
87
+ │ └── tauri.conf.json # generated by configure
88
+ ├── assets/icon-source.png # the 1024px master `iconSource` points at by default
89
+ ├── dist/ # the bundled frontend (a placeholder in remote mode; the app in local mode)
90
+ └── docs/
91
+ ├── BUILDING.md # local dev/build commands and release builds
92
+ ├── LOCAL_APPS.md # packaging a CLI: the sidecar bridge and the bundled frontend
93
+ ├── OAUTH.md # why login needs a system browser + deep link, and how it works here
94
+ ├── MOBILE.md # Android/iOS host requirements and gen/android, gen/apple
95
+ └── APP_STORES.md # Microsoft Store, Mac App Store, iOS App Store, Google Play
96
+ ```
97
+
98
+ ## Why generated config instead of a hand-edited `tauri.conf.json`
99
+
100
+ An app's identity shows up in five places that must agree: the Tauri config, the Rust deep-link
101
+ scheme, the Rust sidecar name, the remote capability's origin scope, and the icon set. Hand-editing
102
+ means five chances to drift. `scripts/configure.mjs` writes all of them from the profile, so the
103
+ profile is the only file anyone edits, and CI can assert the generated files still match it.
104
+
105
+ ## Why a window, not a bundled copy of a remote site
106
+
107
+ In remote mode, `tauri.conf.json`'s main window points its `url` straight at the profile's site over
108
+ HTTPS — there's no local copy of the app's UI to keep in sync with the real site
109
+ (`build.frontendDist` still points at `dist/`, an unused placeholder Tauri's bundler requires to
110
+ exist). The tradeoff: the app needs network access to be useful, same as opening the site in a
111
+ browser tab would. Local mode is the opposite tradeoff — everything is bundled, nothing is fetched.
Binary file
package/bin/cli.js ADDED
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ // native-app-wrapper CLI.
3
+ //
4
+ // init <dir> [--profile <name> | --profile-file <path>]
5
+ // Copy this wrapper into <dir> as a self-contained app, configured for
6
+ // one profile. The copy owns its profile from then on — nothing links
7
+ // back here — which is the point: a wrapped app's identity, icons, and
8
+ // build docs live next to the thing being wrapped, not in this package.
9
+ //
10
+ // configure [--profile <name>]
11
+ // Regenerate the Tauri config, Rust constants, and remote capability
12
+ // from a profile, in place. Run by the pre* npm scripts.
13
+ //
14
+ // icons [--profile <name>]
15
+ // Draw the dependency-free placeholder icon set (see
16
+ // scripts/generate-placeholder-icons.mjs); `npm run icons` instead
17
+ // generates a real set from the profile's iconSource via the Tauri CLI.
18
+
19
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
20
+ import { fileURLToPath } from "node:url";
21
+ import path from "node:path";
22
+ import { configure } from "../scripts/configure.mjs";
23
+ import { generatePlaceholderIcons } from "../scripts/generate-placeholder-icons.mjs";
24
+ import { loadProfile, resolveProfileName } from "../scripts/profile.mjs";
25
+
26
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
27
+
28
+ // What a scaffolded copy is made of. Generated artifacts (tauri.conf.json,
29
+ // generated_config.rs, src-tauri/icons/) are deliberately absent: `init`
30
+ // regenerates them for the new profile instead of copying this package's, so a
31
+ // copy can never inherit another app's identity by accident. Cargo.lock is
32
+ // absent for the same reason — it names this package's crate, and a copy that
33
+ // inherited it would fail its first build on a package/lock mismatch.
34
+ const SCAFFOLD_SOURCES = [
35
+ "bin",
36
+ "scripts",
37
+ "src-tauri/Cargo.toml",
38
+ "src-tauri/build.rs",
39
+ "src-tauri/src/lib.rs",
40
+ "src-tauri/src/main.rs",
41
+ "src-tauri/capabilities/default.json",
42
+ "dist",
43
+ ".gitignore",
44
+ ];
45
+
46
+ // Which docs a copy gets depends on what its profile actually does — an
47
+ // offline CLI app shipping a guide to browser OAuth handoffs is noise that
48
+ // reads as unimplemented functionality.
49
+ function docsFor(profile) {
50
+ const docs = ["docs/BUILDING.md", "docs/APP_STORES.md"];
51
+ if (profile.mode === "local") docs.push("docs/LOCAL_APPS.md");
52
+ if (profile.deepLinkScheme) docs.push("docs/OAUTH.md");
53
+ if (profile.android || profile.ios) docs.push("docs/MOBILE.md");
54
+ return docs;
55
+ }
56
+
57
+ // Same idea for the npm scripts: a desktop-only profile's package.json
58
+ // shouldn't offer android:build.
59
+ function scriptsFor(profile, template) {
60
+ const scripts = { ...template };
61
+ if (!profile.android && !profile.ios) {
62
+ for (const key of Object.keys(scripts)) {
63
+ if (/^(pre)?(android|ios):/.test(key)) delete scripts[key];
64
+ }
65
+ }
66
+ if (profile.sidecar) {
67
+ // The bundler fails outright when no sidecar binary exists for the target
68
+ // triple, so building it is part of running or building the app, not a
69
+ // step to remember.
70
+ scripts["build:sidecar"] = "node scripts/build-sidecar.mjs";
71
+ scripts.predev = "node bin/cli.js configure && node scripts/build-sidecar.mjs";
72
+ scripts["prebuild:desktop"] = "node bin/cli.js configure && node scripts/build-sidecar.mjs";
73
+ } else {
74
+ delete scripts["build:sidecar"];
75
+ }
76
+ delete scripts.init;
77
+ return scripts;
78
+ }
79
+
80
+ function flag(argv, name) {
81
+ const i = argv.indexOf(name);
82
+ if (i === -1) return undefined;
83
+ const value = argv[i + 1];
84
+ if (!value || value.startsWith("--")) throw new Error(`${name} needs a value`);
85
+ return value;
86
+ }
87
+
88
+ function fail(message) {
89
+ console.error(`[native-app-wrapper] ${message}`);
90
+ process.exit(1);
91
+ }
92
+
93
+ function slug(text) {
94
+ return text
95
+ .toLowerCase()
96
+ .replace(/[^a-z0-9]+/g, "-")
97
+ .replace(/^-|-$/g, "");
98
+ }
99
+
100
+ /** Splits argv into positionals and flags, treating `--profile`/`--profile-file` as value flags. */
101
+ function positionals(argv) {
102
+ const out = [];
103
+ for (let i = 0; i < argv.length; i++) {
104
+ if (argv[i] === "--profile" || argv[i] === "--profile-file") {
105
+ i++; // skip the flag's value
106
+ } else if (!argv[i].startsWith("--")) {
107
+ out.push(argv[i]);
108
+ }
109
+ }
110
+ return out;
111
+ }
112
+
113
+ function init(argv) {
114
+ const [targetArg] = positionals(argv);
115
+ if (!targetArg) fail("init needs a target directory: init <dir> [--profile-file <path>]");
116
+
117
+ const targetDir = path.resolve(process.cwd(), targetArg);
118
+ if (existsSync(targetDir) && readdirSync(targetDir).length > 0 && !argv.includes("--force")) {
119
+ fail(`${targetDir} already exists and is not empty — pass --force to overwrite its wrapper files`);
120
+ }
121
+
122
+ const profileFile = flag(argv, "--profile-file");
123
+ const profileName = profileFile
124
+ ? path.basename(profileFile).replace(/\.json$/, "")
125
+ : (flag(argv, "--profile") ?? "example");
126
+
127
+ if (!profileFile && !existsSync(path.join(packageRoot, "profiles", `${profileName}.json`))) {
128
+ fail(`no profile named "${profileName}" in ${path.join(packageRoot, "profiles")}`);
129
+ }
130
+
131
+ const stagedProfilePath = profileFile
132
+ ? path.resolve(process.cwd(), profileFile)
133
+ : path.join(packageRoot, "profiles", `${profileName}.json`);
134
+
135
+ mkdirSync(targetDir, { recursive: true });
136
+ for (const entry of SCAFFOLD_SOURCES) {
137
+ const from = path.join(packageRoot, entry);
138
+ if (!existsSync(from)) continue;
139
+ const to = path.join(targetDir, entry);
140
+ mkdirSync(path.dirname(to), { recursive: true });
141
+ cpSync(from, to, { recursive: true });
142
+ }
143
+
144
+ // The copy gets exactly one profile — its own — so its scripts resolve it
145
+ // with no --profile flag and no ambiguity about which app this tree is.
146
+ mkdirSync(path.join(targetDir, "profiles"), { recursive: true });
147
+ cpSync(stagedProfilePath, path.join(targetDir, "profiles", `${profileName}.json`));
148
+ cpSync(path.join(packageRoot, "profiles", "README.md"), path.join(targetDir, "profiles", "README.md"));
149
+
150
+ const profile = loadProfile(targetDir, profileName);
151
+
152
+ for (const doc of docsFor(profile)) {
153
+ const to = path.join(targetDir, doc);
154
+ mkdirSync(path.dirname(to), { recursive: true });
155
+ cpSync(path.join(packageRoot, doc), to);
156
+ }
157
+
158
+ // package.json is rewritten rather than copied: the copy is a distinct app
159
+ // with its own name and version, and it must not carry this package's.
160
+ const template = JSON.parse(readFileSync(path.join(packageRoot, "package.json"), "utf8"));
161
+ writeFileSync(
162
+ path.join(targetDir, "package.json"),
163
+ `${JSON.stringify(
164
+ {
165
+ ...template,
166
+ name: `${slug(profile.appName)}-native`,
167
+ version: profile.version,
168
+ description: `${profile.appName} packaged as a native desktop app (scaffolded from native-app-wrapper)`,
169
+ private: true,
170
+ bin: undefined,
171
+ scripts: scriptsFor(profile, template.scripts),
172
+ },
173
+ null,
174
+ 2,
175
+ )}\n`,
176
+ );
177
+
178
+ const { written } = configure(targetDir, profileName);
179
+ const icons = generatePlaceholderIcons(targetDir, {
180
+ background: ["#1e293b", "#0b1220"],
181
+ accent: "#f8fafc",
182
+ ...profile.placeholderIcon,
183
+ });
184
+
185
+ // src-tauri/Cargo.toml's crate name is what `tauri build` names the binary,
186
+ // so it has to follow the copy rather than stay "native-app-wrapper".
187
+ const crateName = `${slug(profile.appName)}-native`;
188
+ const cargoPath = path.join(targetDir, "src-tauri", "Cargo.toml");
189
+ writeFileSync(
190
+ cargoPath,
191
+ readFileSync(cargoPath, "utf8")
192
+ .replace(/^name = "native-app-wrapper"$/m, `name = "${crateName}"`)
193
+ .replace(/^name = "native_app_wrapper_lib"$/m, `name = "${crateName.replace(/-/g, "_")}_lib"`)
194
+ .replace(/^description = ".*"$/m, `description = "${profile.appName} packaged as a native app"`),
195
+ "utf8",
196
+ );
197
+ const mainPath = path.join(targetDir, "src-tauri", "src", "main.rs");
198
+ writeFileSync(
199
+ mainPath,
200
+ readFileSync(mainPath, "utf8").replace("native_app_wrapper_lib::run()", `${crateName.replace(/-/g, "_")}_lib::run()`),
201
+ "utf8",
202
+ );
203
+ const rel = path.relative(process.cwd(), targetDir) || ".";
204
+ console.log(`[native-app-wrapper] scaffolded ${profile.appName} into ${rel}/`);
205
+ for (const entry of written) console.log(` wrote ${entry}`);
206
+ console.log(` wrote ${icons.length} placeholder icons into src-tauri/icons/`);
207
+ console.log(`\nNext: cd ${rel} && npm install && npm run dev`);
208
+ }
209
+
210
+ const [command, ...argv] = process.argv.slice(2);
211
+
212
+ try {
213
+ if (command === "init") {
214
+ init(argv);
215
+ } else if (command === "configure") {
216
+ const rootDir = packageRoot;
217
+ const { profile, written } = configure(rootDir, resolveProfileName(rootDir, argv));
218
+ const target = profile.mode === "remote" ? profile.url : "bundled dist/ frontend";
219
+ console.log(`[native-app-wrapper] configured "${profile.name}" (${profile.appName} -> ${target})`);
220
+ for (const entry of written) console.log(` wrote ${entry}`);
221
+ } else if (command === "icons") {
222
+ const profile = loadProfile(packageRoot, resolveProfileName(packageRoot, argv));
223
+ const written = generatePlaceholderIcons(packageRoot, {
224
+ background: ["#1e293b", "#0b1220"],
225
+ accent: "#f8fafc",
226
+ ...profile.placeholderIcon,
227
+ });
228
+ console.log(`[native-app-wrapper] drew ${written.length} placeholder icons for "${profile.name}"`);
229
+ } else {
230
+ console.log(
231
+ [
232
+ "native-app-wrapper",
233
+ "",
234
+ " init <dir> [--profile <name>] [--profile-file <path>] [--force]",
235
+ " Copy this wrapper into <dir> as a standalone app for one profile.",
236
+ "",
237
+ " configure [--profile <name>]",
238
+ " Regenerate tauri.conf.json, the Rust constants, and the remote capability.",
239
+ "",
240
+ " icons [--profile <name>]",
241
+ " Draw the placeholder icon set (no dependencies, no artwork needed).",
242
+ ].join("\n"),
243
+ );
244
+ process.exit(command ? 1 : 0);
245
+ }
246
+ } catch (error) {
247
+ fail(error.message);
248
+ }
@@ -0,0 +1,24 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Loading…</title>
6
+ <style>
7
+ html, body { height: 100%; margin: 0; background: #0b1220; color: #d8d8d8; font-family: system-ui, sans-serif; }
8
+ body { display: flex; align-items: center; justify-content: center; }
9
+ </style>
10
+ </head>
11
+ <body>
12
+ <!--
13
+ In remote mode this file is never shown: the main window's `url` in
14
+ tauri.conf.json points straight at the configured site. Tauri's bundler
15
+ still requires `build.frontendDist` to point at a real, non-empty
16
+ directory, and this doubles as the offline fallback content a future
17
+ retry/offline screen can build on.
18
+
19
+ In local mode this directory IS the app — replace this file with the
20
+ frontend you want bundled. See ../docs/LOCAL_APPS.md.
21
+ -->
22
+ <p>Loading…</p>
23
+ </body>
24
+ </html>
@@ -0,0 +1,129 @@
1
+ # Submitting to app stores
2
+
3
+ This covers what each store needs beyond the installers `npm run build:desktop` (or a CI matrix
4
+ like the one in `BUILDING.md`) already produces.
5
+ None of this is automated — every store below needs an account, credentials only a human on that
6
+ account can provision, and (for Apple) manual review — so treat this as a checklist, not a script.
7
+
8
+ ## Microsoft Store (Windows)
9
+
10
+ Tauri does not build MSIX — it only produces the `.exe` (NSIS) and `.msi` (WiX) installers
11
+ `build:desktop` already produces. The Microsoft Store submission type for that is an **"EXE or MSI
12
+ app"**: the Store hosts a listing that links to your own signed installer rather than a sandboxed
13
+ MSIX package.
14
+
15
+ 1. Build with a Store-specific config overlay that forces the offline WebView2 installer (Store
16
+ review rejects the online/evergreen bootstrapper) and code-sign the installer:
17
+ ```jsonc
18
+ // tauri.microsoftstore.conf.json (create this file, merged over tauri.conf.json)
19
+ { "bundle": { "windows": { "webviewInstallMode": { "type": "offlineInstaller" } } } }
20
+ ```
21
+ ```bash
22
+ npx tauri build --config tauri.microsoftstore.conf.json
23
+ ```
24
+ 2. In [Partner Center](https://partner.microsoft.com/dashboard), register the app under
25
+ **Apps and Games → EXE or MSI app**, using a **Publisher name distinct from the product name**
26
+ (Store review flags the two matching exactly), and supply the installer's silent-install flag
27
+ (NSIS's `/S`, applied automatically by Tauri's installer).
28
+ 3. Submit for certification. Because the Store links to an external installer, your own
29
+ update mechanism (Tauri's updater, once enabled — see `BUILDING.md`) is what actually
30
+ updates users; the Store doesn't auto-update this install type the way it would a native MSIX.
31
+
32
+ A true sandboxed MSIX is possible but needs a separate third-party packaging step on top of
33
+ Tauri's own bundler (e.g. the community `@choochmeque/tauri-windows-bundle` tool) — not something
34
+ this wrapper sets up by default.
35
+
36
+ ## Mac App Store
37
+
38
+ Distinct from notarized-`.dmg` distribution outside the Store (which uses a Developer ID cert and
39
+ `notarytool`) — App Store distribution needs a **3rd Party Mac Developer Application** cert, a
40
+ **3rd Party Mac Developer Installer** cert, **App Sandbox entitlements**, and upload as a signed
41
+ `.pkg`, not a `.dmg`.
42
+
43
+ 1. `src-tauri/Entitlements.plist` (create this; not generated by `configure`) needs at least:
44
+ ```xml
45
+ <?xml version="1.0" encoding="UTF-8"?>
46
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
47
+ <plist version="1.0">
48
+ <dict>
49
+ <key>com.apple.security.app-sandbox</key><true/>
50
+ <key>com.apple.security.network.client</key><true/>
51
+ <!-- Tauri's webview needs JIT; omitting this is a documented cause of
52
+ crash-on-launch after notarization. -->
53
+ <key>com.apple.security.cs.allow-jit</key><true/>
54
+ <key>com.apple.application-identifier</key><string>$TEAM_ID.$IDENTIFIER</string>
55
+ <key>com.apple.developer.team-identifier</key><string>$TEAM_ID</string>
56
+ </dict>
57
+ </plist>
58
+ ```
59
+ and reference it from `tauri.conf.json`'s `bundle.macOS.entitlements`.
60
+ 2. Build, sign, and upload:
61
+ ```bash
62
+ npx tauri build --bundles app --target universal-apple-darwin
63
+ xcrun productbuild --sign "3rd Party Mac Developer Installer: <Your Team> (TEAMID)" \
64
+ --component "src-tauri/target/universal-apple-darwin/release/bundle/macos/<Product Name>.app" \
65
+ /Applications <Product>.pkg
66
+ xcrun altool --upload-app --type macos --file <Product>.pkg \
67
+ --apiKey "$APPLE_API_KEY_ID" --apiIssuer "$APPLE_API_ISSUER"
68
+ ```
69
+ (Apple has been steering uploads toward the newer Transporter CLI / `notarytool`-adjacent
70
+ tooling; verify `altool`'s status against Apple's current documentation before relying on it.)
71
+ 3. In [App Store Connect](https://appstoreconnect.apple.com), register the app with a Bundle ID
72
+ matching `tauri.conf.json`'s `identifier` exactly, create a **Mac App Store** provisioning
73
+ profile (distinct from a Developer ID profile), and generate an API key (`.p8`) for CI/CLI
74
+ auth.
75
+
76
+ ## iOS App Store
77
+
78
+ 1. Build a signed `.ipa`: `npm run ios:build -- --export-method app-store-connect` (needs a
79
+ distribution certificate + provisioning profile configured in the generated Xcode project
80
+ first — see `MOBILE.md`). Output lands at
81
+ `src-tauri/gen/apple/build/arm64/*.ipa`.
82
+ 2. Upload the same way as macOS: `xcrun altool --upload-app --type ios --file *.ipa --apiKey ... --apiIssuer ...`,
83
+ or drag it into Xcode Organizer / the Transporter app. It lands in App Store Connect →
84
+ **TestFlight** automatically — internal testers get it immediately, external testers need a
85
+ lightweight Beta App Review first.
86
+ 3. **Read this before investing in an iOS submission**: Apple's App Store Review Guideline
87
+ **4.2 "Minimum Functionality"** is a real rejection risk for an app that's essentially "load a
88
+ website in a webview" with no native chrome —
89
+ > "Your app should include features, content, and UI that elevate it beyond a repackaged
90
+ > website... Other than catalogs, apps shouldn't primarily be marketing materials,
91
+ > advertisements, web clippings, content aggregators, or a collection of links."
92
+ ([developer.apple.com/app-store/review/guidelines](https://developer.apple.com/app-store/review/guidelines/), §4.2)
93
+
94
+ iOS reviewers test submissions manually and are noticeably stricter here than Android or
95
+ desktop review. To materially reduce rejection risk before submitting, this app needs to be
96
+ more than a URL in a box: native navigation chrome (a real tab bar / back gesture, not just
97
+ in-page nav), push notifications wired to APNs, a native offline-state screen instead of a
98
+ blank WebView on connectivity loss — enough that the app doesn't read as "open Safari to this
99
+ URL." This is a product/design decision for whoever owns the app, not a packaging setting —
100
+ flag it before committing engineering time to an iOS submission specifically.
101
+
102
+ ## Google Play Store (Android)
103
+
104
+ 1. **Format**: Google requires the **Android App Bundle** (`.aab`, not a plain `.apk`) for new
105
+ apps — `npm run android:build` produces one; a CI Android job can build it (see `MOBILE.md`).
106
+ 2. **Play App Signing** is mandatory for new apps on AAB: Google holds the app signing key, you
107
+ keep only an upload key. Enroll during your first release in
108
+ [Play Console](https://play.google.com/console).
109
+ 3. **Target API level** (current requirements as of this writing):
110
+ - New apps/updates must target **Android 16 (API 36)** by **August 31, 2026** (extension to
111
+ November 1, 2026 available on request in Play Console). The profile's
112
+ `android.minSdkVersion` is the *minimum* supported OS, unrelated to the *target* level —
113
+ set the target via `gen/android`'s Gradle config once you run `android:init`.
114
+ - Existing published apps need at least **API 35** to stay visible to users on newer Android
115
+ versions.
116
+ - Build with **NDK r28+** (see `MOBILE.md`) — Play now requires 16KB-page-aligned native
117
+ libraries for new apps/updates targeting recent devices, which NDK ≥28 produces automatically.
118
+ 4. Submission flow: create the app listing, enable Play App Signing, upload the signed `.aab` to
119
+ an internal/closed testing track first, complete the **Data Safety** form and content-rating
120
+ questionnaire (a remote-mode app relays OAuth data through its login handoff, and a local-mode app may read
121
+ system state — describe whichever applies accurately), then promote to production. Play has no equivalent to Apple's 4.2 guideline
122
+ strictly enforced against webview wrappers.
123
+
124
+ ## GitHub Releases (no store at all)
125
+
126
+ The default, and what `BUILDING.md`'s CI matrix produces: desktop installers attach directly to a
127
+ GitHub Release. No review process, no developer-account fees, works today — the tradeoff is no store discoverability and users see an
128
+ "unidentified developer" / SmartScreen warning on first launch unless the signing secrets above
129
+ are configured.
@@ -0,0 +1,134 @@
1
+ # Building
2
+
3
+ Every command below is run from the app directory — either this package (which builds
4
+ `profiles/example.json` as a smoke test) or a copy `node bin/cli.js init` scaffolded elsewhere.
5
+
6
+ ## Prerequisites
7
+
8
+ Tauri compiles a Rust binary against the host's native webview, so each OS needs its own toolchain
9
+ before anything here works. From [Tauri's prerequisites guide](https://v2.tauri.app/start/prerequisites/):
10
+
11
+ | Host | Needs |
12
+ |---|---|
13
+ | **Windows** | [Microsoft C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) (Desktop development with C++), WebView2 (preinstalled on Windows 10 1803+/11), and Rust. |
14
+ | **macOS** | Xcode Command Line Tools (`xcode-select --install`) and Rust. |
15
+ | **Linux** | `libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev build-essential curl wget file libssl-dev` (Debian/Ubuntu names; see the guide for Fedora/Arch), and Rust. |
16
+
17
+ Plus [Rust](https://rustup.rs) 1.77.2 or newer and Node 18+ everywhere.
18
+
19
+ ## Local dev
20
+
21
+ ```bash
22
+ npm install
23
+ npm run dev
24
+ ```
25
+
26
+ `predev` regenerates the Tauri config from the profile first, so a profile edit takes effect
27
+ without a separate step. In remote mode the window opens on the profile's `url` — there's no local
28
+ frontend to serve, so a dev server for the wrapped site is unrelated (to test against one, point
29
+ the profile's `url` at `http://localhost:3000` temporarily, re-run `npm run configure`, and don't
30
+ commit that change). In local mode the window opens on `dist/`.
31
+
32
+ ## Release build
33
+
34
+ ```bash
35
+ npm run build:desktop
36
+ ```
37
+
38
+ Output lands under `src-tauri/target/release/bundle/`:
39
+
40
+ | Platform | Artifacts |
41
+ |---|---|
42
+ | Windows | `msi/*.msi` (WiX), `nsis/*-setup.exe` |
43
+ | macOS | `dmg/*.dmg`, `macos/*.app` |
44
+ | Linux | `deb/*.deb`, `rpm/*.rpm`, `appimage/*.AppImage` |
45
+
46
+ **Each installer must be built on its own OS.** Tauri does not cross-compile: a Windows `.msi`
47
+ needs a Windows host, a `.dmg` needs macOS. That's what makes a CI matrix (below) the normal way to
48
+ produce a full set.
49
+
50
+ macOS builds default to the host's architecture; add `--target universal-apple-darwin` (and
51
+ `rustup target add x86_64-apple-darwin aarch64-apple-darwin`) for a universal binary that runs on
52
+ both Intel and Apple Silicon.
53
+
54
+ If the profile bundles a sidecar, build it **before** `build:desktop` — the bundle step fails
55
+ outright when no binary exists for the target triple. See `LOCAL_APPS.md`.
56
+
57
+ ## Android / iOS
58
+
59
+ See `MOBILE.md` — needs host tooling this package can't install for you. Sidecars are desktop-only,
60
+ so a local-mode app with a `sidecar` has no mobile story; a remote-mode app does.
61
+
62
+ ## CI
63
+
64
+ Building for three OSes means three runners. The shape that works:
65
+
66
+ ```yaml
67
+ strategy:
68
+ matrix:
69
+ include:
70
+ - { os: windows-latest, args: "" }
71
+ - { os: macos-latest, args: "--target universal-apple-darwin" }
72
+ - { os: ubuntu-22.04, args: "" }
73
+ runs-on: ${{ matrix.os }}
74
+ steps:
75
+ # ... checkout, node, rust, and the Linux apt packages from the table above ...
76
+ - uses: tauri-apps/tauri-action@v0
77
+ with:
78
+ projectPath: <path to the app directory>
79
+ args: ${{ matrix.args }}
80
+ ```
81
+
82
+ [`tauri-apps/tauri-action`](https://github.com/tauri-apps/tauri-action) handles the build and can
83
+ attach the artifacts to a GitHub Release. `packages/about-system-info`'s
84
+ `.github/workflows/about-system-desktop.yml` is a complete working instance of this, including the
85
+ sidecar build step that has to run first.
86
+
87
+ Worth asserting in CI: that `src-tauri/tauri.conf.json` and `src-tauri/src/generated_config.rs`
88
+ still match what `configure` produces from the profile. Re-running configure and checking
89
+ `git diff --exit-code` catches hand-edits that have drifted from the profile they're generated
90
+ from.
91
+
92
+ ## Code signing
93
+
94
+ Unsigned installers work, but users get an "unidentified developer" (macOS) or SmartScreen
95
+ (Windows) warning on first launch. `tauri-action` picks up signing automatically once the relevant
96
+ secrets exist:
97
+
98
+ - **macOS**: `APPLE_CERTIFICATE`, `APPLE_CERTIFICATE_PASSWORD`, `APPLE_SIGNING_IDENTITY`, and for
99
+ notarization `APPLE_ID` + `APPLE_PASSWORD` + `APPLE_TEAM_ID` (or `APPLE_API_KEY`,
100
+ `APPLE_API_ISSUER`, `APPLE_API_KEY_PATH`).
101
+ - **Windows**: an Authenticode certificate, configured through `bundle.windows.certificateThumbprint`
102
+ or a signing command.
103
+
104
+ See `APP_STORES.md` for where those credentials come from.
105
+
106
+ ## Auto-update (off by default)
107
+
108
+ `tauri-plugin-updater` is in `Cargo.toml`, but a profile with no `updater` block never registers
109
+ it and `tauri.conf.json` gets no `plugins.updater` section. That is not the same as switching it
110
+ off with `"active": false` — the plugin reads `pubkey` during *initialization* and panics if it
111
+ isn't there, so an inert-looking config block is a crash on launch, not a disabled feature.
112
+
113
+ To turn updates on:
114
+
115
+ 1. `npx @tauri-apps/cli@2 signer generate` — keep the private key, note the public key.
116
+ 2. Add to the profile:
117
+ ```json
118
+ "updater": {
119
+ "pubkey": "<the public key>",
120
+ "endpoints": ["https://github.com/<owner>/<repo>/releases/latest/download/latest.json"]
121
+ }
122
+ ```
123
+ and re-run `configure`. (Both fields are required; the profile validator rejects one without
124
+ the other.)
125
+ 3. Store the private key as a `TAURI_SIGNING_PRIVATE_KEY` secret, and its password as
126
+ `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` if you set one.
127
+ 4. Set `tauri-action`'s `includeUpdaterJson: true` so releases carry the `latest.json` manifest
128
+ the updater polls.
129
+
130
+ ## Cutting a release
131
+
132
+ Bump `version` in the profile, run `npm run configure`, commit, then tag. The version in
133
+ `tauri.conf.json` is what ends up in the installer's metadata and in every store listing, so the
134
+ profile is the single place it's set.