cargo-rahti-native 0.0.1

Optional Windows and Android packaging for Rahti applications: initialize, check prerequisites, run and package a Tauri shell around an existing Rahti app.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
//! The files `init` writes into `native/`.
//!
//! Everything here is **application-owned once written**. The identifier, the
//! icons, the window, the Tauri permissions and the commands are decisions a
//! project makes, and a project that edits them is doing the thing this
//! scaffold exists to allow. `init` refuses to overwrite an edited file unless
//! `--force` names it — the same contract `cargo rahti upgrade` holds to.
//!
//! ## Why the shell is a separate cargo package
//!
//! It is the one package in a Rahti project that depends on Tauri, and it sits
//! outside the project's workspace (`exclude = ["native"]`). So a web-only
//! project's `cargo check --workspace`, `cargo test --workspace` and
//! `cargo build` never resolve, download or compile a native dependency —
//! which is what "native support is opt-in" has to mean to be worth saying.
//!
//! ## What the shell actually does
//!
//! It is deliberately thin. It resolves the packaged paths, stages the assets,
//! restores the session key, starts the application's own
//! `initialize_application`, serves it on a loopback socket, and opens a
//! window on it. Every one of those steps is a call into `rahti-native` or
//! into the application; nothing about Rahti is reimplemented here.

use rahti_native::NativeConfig;

use crate::project::Project;

/// One generated file.
///
/// There is no "framework-owned" flag, because there is no file here Rahti
/// keeps: every one of them is regenerated while its recorded hash still
/// matches and left alone the moment it does not. That is one rule for the
/// whole directory, which is a rule somebody can hold in their head.
pub struct File {
    /// Relative to the project root.
    pub path: String,
    pub contents: String,
}

impl File {
    fn new(path: impl Into<String>, contents: impl Into<String>) -> Self {
        File {
            path: path.into(),
            contents: contents.into(),
        }
    }
}

/// Every text file of the shell.
///
/// The icons are binary and are written separately — see [`crate::icons`].
pub fn files(project: &Project, config: &NativeConfig) -> Vec<File> {
    vec![
        File::new("native/Cargo.toml", cargo_toml(project, config)),
        File::new("native/build.rs", BUILD_RS.to_string()),
        File::new("native/tauri.conf.json", tauri_conf(config)),
        File::new("native/capabilities/default.json", capabilities()),
        File::new("native/permissions/rahti-native.json", permissions()),
        File::new("native/src/lib.rs", lib_rs(project, config)),
        File::new("native/src/main.rs", main_rs(project)),
        File::new("native/dist/index.html", DIST_INDEX.to_string()),
        File::new("native/.gitignore", GITIGNORE.to_string()),
        File::new("native/README.md", readme(config)),
    ]
}

// ------------------------------------------------------------ Cargo.toml

fn cargo_toml(project: &Project, config: &NativeConfig) -> String {
    let package = format!("{}-native", project.package);
    // `_lib`, so the library target and the binary target do not write the
    // same debug-info filename — cargo warns about the collision, and on
    // Windows the two would overwrite each other's `.pdb`. Tauri's own
    // template does the same thing for the same reason.
    let lib = format!("{}_native_lib", project.lib);

    format!(
        r#"# The native shell. Generated by `cargo rahti native init`, and yours from
# here: the dependencies below are the ones the generated shell uses, and
# anything else a native capability needs is added by you.
#
# Deliberately outside the project's workspace. This is the only package in a
# Rahti project that depends on Tauri, and the project root excludes this
# directory so that a web build never resolves one.
[package]
name = "{package}"
version = "{version}"
edition = "2024"
publish = false

# Its own workspace, so `cargo` inside native/ does not walk up into the
# application's and try to make this a member of it.
[workspace]

# `cdylib` is what Android loads — a packaged Android application is a Java
# activity that dlopens this library. `staticlib` and `rlib` are what iOS and
# the desktop binary want. All three, because the same source is all three.
[lib]
name = "{lib}"
crate-type = ["staticlib", "cdylib", "rlib"]

[build-dependencies]
tauri-build = {{ version = "2", features = [] }}

[dependencies]
tauri = {{ version = "2", features = [] }}
# The two plugins the generated commands use. `opener` hands a URL to the
# user's own browser; `dialog` opens the platform file picker. Both are
# official Tauri plugins, and neither can open anything the user did not
# choose.
tauri-plugin-dialog = "2"
tauri-plugin-opener = "2"
# The application's `public/`, compiled into the binary. Android's package
# resources are zip entries and Tauri reports their directory as the URI
# `asset://localhost/`, which is not a path a file server can open — so the
# assets travel in the executable and are written to internal storage on first
# launch. See `rahti_native::stage_embedded_assets`.
include_dir = "0.7"

# The application itself. The shell links its library and calls
# `initialize_application`, which is the same function `src/main.rs` calls —
# so the web binary and the package cannot drift apart.
{app_dep}

# The platform-neutral half of native support: packaged paths, the loopback
# server, the session key, the capability allowlist.
{native_dep}

serde = {{ version = "1", features = ["derive"] }}
tokio = {{ version = "1", features = ["rt-multi-thread"] }}
"#,
        version = config.version,
        app_dep = format!("{} = {{ path = \"..\" }}", project.package),
        native_dep = native_dependency(config),
    )
}

/// `rahti-native`, by version or by path.
///
/// A published version is what a project gets. `local` is for working on the
/// framework itself, where the crate being packaged against is the one in the
/// checkout rather than the one on crates.io — and it is recorded in
/// `rahti.native.json` rather than passed each run, because this manifest is
/// regenerated and a forgotten flag would silently move the shell back.
///
/// The path is written relative to `native/`, which is one level below the
/// project root the configured path is relative to.
fn native_dependency(config: &NativeConfig) -> String {
    let Some(local) = &config.local else {
        return format!("rahti-native = {{ version = \"{}\" }}", NATIVE_VERSION);
    };

    let checkout = local.trim_end_matches('/');
    let path = if std::path::Path::new(checkout).is_absolute() {
        format!("{checkout}/crates/rahti-native")
    } else if checkout == "." {
        // The project *is* the checkout, which is the framework's own
        // demonstration application.
        "../crates/rahti-native".to_string()
    } else {
        format!("../{checkout}/crates/rahti-native")
    };

    format!("rahti-native = {{ path = \"{path}\" }}")
}

/// The `rahti-native` release this tool generates against.
///
/// The two ship in lockstep for the reason the rest of the framework does: the
/// shell this writes calls functions in that crate, and a mismatch would be a
/// compile error naming generated code.
const NATIVE_VERSION: &str = env!("CARGO_PKG_VERSION");

const BUILD_RS: &str = r#"// Generated by cargo-rahti-native — do not edit.
//
// Tauri's build script. It reads tauri.conf.json, generates the permission
// schemas under gen/, and on Windows embeds the icon and the manifest into the
// executable.
fn main() {
    tauri_build::build()
}
"#;

// ------------------------------------------------------- tauri.conf.json

fn tauri_conf(config: &NativeConfig) -> String {
    // Written as text rather than through serde so that the comments a reader
    // needs can... they cannot: JSON has no comments. It is written as text so
    // the field order is the one Tauri's own documentation uses, which is what
    // somebody comparing the two will expect.
    let icons = ["icons/32x32.png", "icons/128x128.png", "icons/icon.ico"]
        .map(|i| format!("      \"{i}\""))
        .join(",\n");

    format!(
        r#"{{
  "$schema": "https://schema.tauri.app/config/2",
  "productName": {product},
  "version": {version},
  "identifier": {identifier},
  "build": {{
    "frontendDist": "./dist"
  }},
  "app": {{
    "withGlobalTauri": true,
    "windows": [],
    "security": {{
      "csp": {csp}
    }}
  }},
  "bundle": {{
    "active": true,
    "targets": ["nsis", "msi"],
    "icon": [
{icons}
    ],
    "android": {{
      "minSdkVersion": {min_sdk}
    }}
  }}
}}
"#,
        product = json(&config.product_name),
        version = json(&config.version),
        identifier = json(&config.identifier),
        // The same policy the embedded server sends, so that the shell's own
        // placeholder document and the application's pages cannot be governed
        // by two different rules. The one that reaches a page is the header —
        // see `rahti_native::security_headers`.
        csp = json(&config.security.csp),
        min_sdk = config.android.min_sdk,
    )
}

/// The application's own commands, as a Tauri permission.
///
/// Tauri's ACL namespaces a permission by prefix: `dialog:allow-open` is the
/// dialog plugin's, and an identifier with **no** prefix is the application's
/// own. Commands defined with `#[tauri::command]` need one of those before a
/// page may call them — which for a Rahti application is not optional, because
/// its pages are served from `http://127.0.0.1` and Tauri treats that as a
/// remote origin rather than as its own asset protocol.
///
/// Without this file every `invoke` is refused with
/// `<command> not allowed. Plugin not found`, which names neither the cause
/// nor the fix.
///
/// Generated from [`rahti_native::commands`], so the browser allowlist, the
/// Rust allowlist and the Tauri ACL cannot drift apart.
fn permissions() -> String {
    let allowed: Vec<String> = rahti_native::commands()
        .iter()
        .map(|command| format!("      \"{}\"", command.name))
        .collect();

    format!(
        r#"{{
  "permission": [
    {{
      "identifier": "allow-rahti-native-commands",
      "description": "The native commands Rahti exposes to the application's own pages. Every entry is also on the Rust allowlist in `rahti_native::commands()`; a command needs both.",
      "commands": {{
        "allow": [
{allowed}
        ],
        "deny": []
      }}
    }}
  ]
}}
"#,
        allowed = allowed.join(
            ",
"
        )
    )
}

/// `"windows": []` is not an oversight.
///
/// The window is created in Rust, after the server has bound, because its URL
/// contains a port the operating system assigns at launch and a token that
/// exists only for this launch. Neither can be written into a file.
fn capabilities() -> String {
    r#"{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "What the application's own pages may ask the operating system for. Every entry here is a decision: an XSS in a page reaches everything on this list.",
  "windows": ["main"],
  "remote": {
    "urls": ["http://127.0.0.1:*"]
  },
  "permissions": [
    "core:default",
    "opener:allow-open-url",
    "dialog:allow-open",
    "allow-rahti-native-commands"
  ]
}
"#
    .to_string()
}

// ----------------------------------------------------------- src/lib.rs

fn lib_rs(project: &Project, config: &NativeConfig) -> String {
    let cookie = match &config.auth.cookie_name {
        Some(name) => format!("Some({})", json(name)),
        None => "None".to_string(),
    };

    format!(
        r#"//! The native shell.
//!
//! Generated by `cargo rahti native init`. It starts the same application the
//! web binary starts, serves it on a loopback socket, and opens a WebView on
//! it.
//!
//! **The WebView is a WebView.** What it draws is HTML rendered by the
//! application's own Rust, laid out by the system web engine — WebView2 on
//! Windows, Android System WebView on Android. They are not native operating
//! system controls and this shell does not turn them into any.
//!
//! ## The order of a launch
//!
//! Every line below is in the order it is for a reason:
//!
//! 1. resolve where this installation keeps its files;
//! 2. stage the packaged assets into internal storage;
//! 3. put the paths, the session key and the release flags in the environment
//!    — before anything reads them;
//! 4. **bind** the loopback socket, so the port is real;
//! 5. build the application's router;
//! 6. **serve** it;
//! 7. only then create the window.
//!
//! Steps 4 and 7 in that order are the startup race, and the reason the WebView
//! never opens on a port nothing is listening to.

use std::time::Duration;

use rahti_native::{{AppPaths, DatabaseMode, EmbeddedServer, LaunchToken, Platform, RunningServer}};
use tauri::{{Manager, WebviewUrl, WebviewWindowBuilder}};

/// Written from rahti.native.json. Regenerate with `cargo rahti native init`.
const IDENTIFIER: &str = {identifier};
const VERSION: &str = {version};
const WINDOW_TITLE: &str = {title};
const WINDOW_WIDTH: f64 = {width}.0;
const WINDOW_HEIGHT: f64 = {height}.0;
const WINDOW_RESIZABLE: bool = {resizable};
const CONTENT_SECURITY_POLICY: &str = {csp};

/// The application's `public/`, compiled into this binary.
///
/// Not read from disk at runtime, and that is the whole point. Android's
/// package resources are entries in a zip, and Tauri reports their location as
/// the URI `asset://localhost/` — not a directory, not something a file server
/// can open. A shell that pointed `ServeDir` at it would start, bind, open a
/// window, and 404 every stylesheet and the browser runtime.
///
/// So the bytes travel inside the executable and are written to application
/// storage on first launch. One code path, both platforms.
static PUBLIC: include_dir::Dir<'_> =
    include_dir::include_dir!("$CARGO_MANIFEST_DIR/../public");

/// The project's own `public/` on the machine that built this.
///
/// Used by a development build only, so a stylesheet edit is visible without a
/// rebuild — `rahti`'s watcher is looking at that directory. A release package
/// never consults it, and therefore never depends on a path from the build
/// machine.
const PROJECT_PUBLIC: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../public");

/// The project's `AUTH_COOKIE_NAME`. A name, never the key — the key is
/// generated on this device at first launch and never leaves it.
const AUTH_COOKIE_NAME: Option<&str> = {cookie};

/// Whether this application's database runs inside the package.
const DATABASE_MODE: DatabaseMode = DatabaseMode::{database_mode};

/// Refuse requests to the embedded server that did not come from this launch.
///
/// `127.0.0.1` is unreachable from the network and reachable by every process
/// on the machine. From `security.loopbackToken` in rahti.native.json.
const LOOPBACK_TOKEN: bool = {loopback_token};

/// Held for the life of the process so the server can be stopped when the last
/// window closes.
struct Server(std::sync::Mutex<Option<RunningServer>>);

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {{
    // A release package states its development flags rather than inheriting
    // them: an installed application inherits the environment of whoever
    // launched it.
    rahti_native::harden_release();
    rahti_native::install_csp(CONTENT_SECURITY_POLICY);

    tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .plugin(tauri_plugin_dialog::init())
        .invoke_handler(tauri::generate_handler![
            platform,
            app_version,
            app_data_dir,
            open_external,
            choose_file
        ])
        .setup(|app| {{
            match start(app.handle()) {{
                Ok(server) => {{
                    let url = server.base_url();
                    app.manage(Server(std::sync::Mutex::new(Some(server))));
                    open_window(app.handle(), &url)?;
                    Ok(())
                }}
                // A window with nothing in it explains nothing. Startup
                // returns its failure rather than exiting on it precisely so
                // that this line can exist.
                Err(message) => Err(message.into()),
            }}
        }})
        .build(tauri::generate_context!())
        .expect("the native shell could not be built")
        .run(|app, event| {{
            if let tauri::RunEvent::ExitRequested {{ .. }} = event {{
                stop(app);
            }}
        }});
}}

/// Steps 1 to 6.
fn start(app: &tauri::AppHandle) -> Result<RunningServer, String> {{
    // Tauri knows where the operating system put this installation's
    // directories — on Android only the Java side does, which is why they are
    // asked for rather than derived.
    let resolver = app.path();
    let data = resolver.app_local_data_dir().map_err(describe)?;
    let cache = resolver.app_cache_dir().map_err(describe)?;
    let resources = resolver.resource_dir().map_err(describe)?;

    let paths = AppPaths::from_host(IDENTIFIER, data, cache, resources).map_err(describe)?;
    paths.prepare().map_err(describe)?;

    // A development build serves the project's own `public/` directly, so a
    // stylesheet edit shows up without a rebuild. Anything else writes the
    // embedded copy into internal storage, once per version, replaced whole on
    // an upgrade so a stale framework asset cannot outlive the version that
    // shipped it.
    let source = std::path::Path::new(PROJECT_PUBLIC);
    let public = if cfg!(debug_assertions) && source.is_dir() {{
        source.to_path_buf()
    }} else {{
        let assets = embedded_assets();
        rahti_native::stage_embedded_assets(&assets, &paths.public(), VERSION)
            .map_err(describe)?;
        paths.public()
    }};

    // Before the router is built, because it reads RAHTI_PUBLIC_DIR as it goes
    // up and RAHTI_SPILL_DIR on the first upload that spills.
    paths.apply_environment(public.as_ref());

    // Only when the database is one that can live inside the package. A
    // project on PostgreSQL keeps its own DATABASE_URL: rewriting it to a
    // local SQLite file would start the application against an empty database
    // that looked like a working one.
    if matches!(DATABASE_MODE, DatabaseMode::SqliteLocal) {{
        paths.apply_sqlite_database_url();
    }}

    // Generated on this device at first launch, kept in protected application
    // storage, and restored before the auth policy is built — a key that
    // changed per launch would sign every user out at every restart.
    rahti_native::install_session_secret(&paths, AUTH_COOKIE_NAME).map_err(describe)?;

    tauri::async_runtime::block_on(async {{
        // Bound before anything is told to go there.
        let server = EmbeddedServer::bind().await.map_err(describe)?;

        let application = {app_lib}::initialize_application()
            .await
            .map_err(|e| format!("the application could not start ({{}}): {{e}}", e.step))?;

        // The gate and the security headers, in that order — see
        // `rahti_native::secure`. One call, so the shell names no axum type
        // and cannot get the layer order backwards.
        let router = rahti_native::secure(application.router, LOOPBACK_TOKEN);

        let running = server.serve(router);
        running.wait_until_ready().await.map_err(describe)?;
        Ok(running)
    }})
}}

/// Step 7.
fn open_window(app: &tauri::AppHandle, base_url: &str) -> tauri::Result<()> {{
    // The launch URL carries this launch's token once. The gate turns it into
    // a cookie and redirects to the clean URL, so it is not left in the
    // address bar or in a `Referer`.
    let url = LaunchToken::launch_url(base_url);
    let url: tauri::Url = url.parse().expect("a loopback URL");
    let handle = app.clone();

    WebviewWindowBuilder::new(app, "main", WebviewUrl::External(url))
        .title(WINDOW_TITLE)
        .inner_size(WINDOW_WIDTH, WINDOW_HEIGHT)
        .resizable(WINDOW_RESIZABLE)
        // The bridge, injected before any page script runs. It attaches
        // `pp.native` to the PulsePoint runtime when the bundle publishes it;
        // the runtime bundle itself is untouched.
        .initialization_script(&rahti_native::bridge_script(
            Platform::current().name(),
            VERSION,
        ))
        // A link to somewhere else opens in the user's own browser, and this
        // window stays where it is. A privileged WebView that navigated to an
        // external page would be running somebody else's HTML with the native
        // bridge attached to it.
        //
        // Returning `false` cancels the navigation, so anything that is
        // neither this server nor an ordinary web URL simply does not happen.
        .on_navigation(move |url| {{
            let target = url.to_string();
            if target.starts_with("http://127.0.0.1:") {{
                return true;
            }}
            if rahti_native::is_external_url(&target) {{
                let _ =
                    tauri_plugin_opener::OpenerExt::opener(&handle).open_url(target, None::<&str>);
            }}
            false
        }})
        .build()?;

    Ok(())
}}

/// Stop the server when the application exits.
///
/// Rahti's shutdown broadcast ends the long-lived responses first — the dev
/// event stream, every open WebSocket — because a graceful shutdown would
/// otherwise wait for connections that were never going to close. Ctrl+C is
/// not involved: a packaged GUI never receives one, and Android's lifecycle
/// has nothing like it.
fn stop(app: &tauri::AppHandle) {{
    let Some(state) = app.try_state::<Server>() else {{
        return;
    }};
    let Some(server) = state.0.lock().ok().and_then(|mut held| held.take()) else {{
        return;
    }};
    tauri::async_runtime::block_on(async move {{
        if let Err(e) = server.shutdown(Duration::from_secs(5)).await {{
            eprintln!("rahti native: {{e}}");
        }}
    }});
}}

/// Every embedded file, flattened with `/` separators.
///
/// `include_dir` already normalizes to forward slashes on Windows, but the
/// paths become directories under application storage either way, so it is
/// stated rather than assumed.
fn embedded_assets() -> Vec<rahti_native::EmbeddedAsset<'static>> {{
    fn walk<'a>(
        dir: &'a include_dir::Dir<'a>,
        out: &mut Vec<rahti_native::EmbeddedAsset<'a>>,
    ) {{
        for file in dir.files() {{
            out.push(rahti_native::EmbeddedAsset {{
                path: file.path().to_str().unwrap_or_default(),
                bytes: file.contents(),
            }});
        }}
        for child in dir.dirs() {{
            walk(child, out);
        }}
    }}

    let mut out = Vec::new();
    walk(&PUBLIC, &mut out);
    out
}}

fn describe(error: impl std::fmt::Display) -> String {{
    error.to_string()
}}

// ------------------------------------------------------------- commands
//
// The whole native surface, and it is short on purpose. In a browser an XSS is
// a stolen session; here it is a stolen session and everything below. Nothing
// runs a program, and nothing reads a path the page names — `choose_file`
// returns what the *user* picked, which is the difference the security model
// rests on.
//
// A command has to be in `rahti_native::commands()` and in
// `capabilities/default.json` before it works. Both, on purpose.

#[tauri::command]
fn platform() -> &'static str {{
    Platform::current().name()
}}

#[tauri::command]
fn app_version() -> &'static str {{
    VERSION
}}

/// Where this installation keeps its data — for showing the user, in a
/// settings page or a support message.
#[tauri::command]
fn app_data_dir(app: tauri::AppHandle) -> Result<String, String> {{
    app.path()
        .app_local_data_dir()
        .map(|dir| dir.display().to_string())
        .map_err(describe)
}}

/// Hand a URL to the user's own browser or mail client.
///
/// Refused unless it is `http`, `https` or `mailto`. A `file:` URL opens
/// whatever the shell associates with the extension, which on Windows includes
/// executables; `javascript:` and `data:` are ways back into a privileged
/// context.
#[tauri::command]
fn open_external(app: tauri::AppHandle, url: String) -> Result<(), String> {{
    if !rahti_native::is_external_url(&url) {{
        return Err("that is not a URL this application will open".to_string());
    }}
    tauri_plugin_opener::OpenerExt::opener(&app)
        .open_url(url, None::<&str>)
        .map_err(describe)
}}

/// Open the platform file picker and return what the user chose.
///
/// The page cannot name a path, cannot list a directory, and learns nothing
/// unless a human picked it. An ordinary `<input type="file">` still works and
/// is the right choice on any page that does not need a path back.
#[tauri::command]
async fn choose_file(app: tauri::AppHandle, extensions: Option<Vec<String>>) -> Option<String> {{
    use tauri_plugin_dialog::DialogExt;

    let mut dialog = app.dialog().file();
    if let Some(extensions) = &extensions {{
        let filters: Vec<&str> = extensions.iter().map(String::as_str).collect();
        dialog = dialog.add_filter("Files", &filters);
    }}
    dialog.blocking_pick_file().map(|path| path.to_string())
}}
"#,
        identifier = json(&config.identifier),
        version = json(&config.version),
        title = json(&config.window.title),
        width = config.window.width,
        height = config.window.height,
        resizable = config.window.resizable,
        csp = json(&config.security.csp),
        cookie = cookie,
        database_mode = match config.database.mode {
            rahti_native::DatabaseMode::SqliteLocal => "SqliteLocal",
            rahti_native::DatabaseMode::Remote => "Remote",
        },
        loopback_token = config.security.loopback_token,
        app_lib = project.lib,
    )
}

const MAIN_RS: &str = r#"// Generated by cargo-rahti-native — do not edit.
//
// The desktop binary. Everything is in the library beside it, because Android
// has no `main`: the operating system loads the library and calls the entry
// point `#[tauri::mobile_entry_point]` generates. One `run`, two hosts.

// No console window behind the application on Windows, in a release build.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

fn main() {
    APP_LIB::run()
}
"#;

/// The placeholder document Tauri bundles.
///
/// Tauri's bundler requires a frontend directory, and this project's frontend
/// is served by the embedded server rather than shipped as files — so the
/// directory exists, holds one page, and is never shown: the window is created
/// on the loopback URL.
const DIST_INDEX: &str = r#"<!doctype html>
<meta charset="utf-8">
<title>Starting…</title>
<p>Starting…</p>
"#;

const GITIGNORE: &str = r#"# Build output.
/target
/dist/.vite

# Development diagnostics. A `dev` run has this directory as its working
# directory, so the application writes its log here rather than at the project
# root — where the project's own .gitignore is anchored and would not catch it.
# Never commit it and never ship it: it carries rendered values, error
# messages and URLs.
/.rahti

# Generated by tauri-build on every build.
/gen

# Android and iOS projects, generated by `cargo tauri android init`. They are
# regenerated rather than reviewed, and the Android one carries absolute paths
# from the machine that created it.
/gen/android
/gen/apple

# Never commit a keystore or a signing certificate. Release signing is
# configured through the environment — see native/README.md.
*.keystore
*.jks
*.p12
*.pfx
key.properties
keystore.properties
"#;

fn readme(config: &NativeConfig) -> String {
    format!(
        r#"# {product} — native package

Generated by `cargo rahti native init`. **These files are yours now.** Rahti
will not replace one you have edited unless you name it:

```text
cargo rahti native init --force
```

## What is here

| Path | What it is |
| --- | --- |
| `tauri.conf.json` | The identifier, the version, the bundle targets, the icons. |
| `capabilities/default.json` | What the application's pages may ask the operating system for. |
| `src/lib.rs` | The shell: paths, server, window, and the five native commands. |
| `src/main.rs` | The desktop binary. Android calls the library instead. |
| `icons/` | **Placeholders.** Replace them before you ship anything. |
| `dist/` | A page that is never shown; Tauri's bundler requires a frontend directory. |

## Icons

The generated icons are the Rahti mark, at every size Windows and Android ask
for. That is a real icon set rather than a placeholder, and it is still not
yours — replace it. Tauri's own tool takes a single square PNG and writes every
size both platforms want:

```bash
cargo tauri icon path/to/icon.png
```

## Release signing

Nothing about signing is in any committed file, and nothing should be. Both
platforms read it from the environment of the build.

**Windows** — a PFX certificate, base64 encoded:

```text
RAHTI_NATIVE_WINDOWS_CERTIFICATE
RAHTI_NATIVE_WINDOWS_CERTIFICATE_PASSWORD
```

**Android** — a keystore and its alias:

```text
RAHTI_NATIVE_ANDROID_KEYSTORE
RAHTI_NATIVE_ANDROID_KEYSTORE_PASSWORD
RAHTI_NATIVE_ANDROID_KEY_ALIAS
RAHTI_NATIVE_ANDROID_KEY_PASSWORD
```

On Android these are wired into the generated Gradle project for you — Tauri
reads no signing key from the environment there, so a keystore passed and
assumed would produce an unsigned release. The details land in
`gen/android/keystore.properties`, which is ignored.

`cargo rahti native doctor --release --target android` says which of them are
missing. `cargo rahti native build --target android --debug` produces a package
that installs without any of them, which is what testing wants.

## Adding a native capability

Three places, and all three on purpose:

1. a `#[tauri::command]` in `src/lib.rs`, and in `generate_handler!`;
2. an entry in `rahti_native::commands()`, which is what `pp.native.invoke`
   checks against;
3. a permission in `capabilities/default.json`, which is what Tauri enforces.

Before you add one, assume the page calling it has been compromised, because
the command has to be safe in that case too. That is why there is no command
that runs a program and none that reads a path the page names.

## The identifier

`{identifier}` is in `tauri.conf.json`, in `rahti.native.json` and in
`src/lib.rs`. Changing it makes a different application: on Android it installs
beside the old one rather than over it, and on both platforms the new one
starts with an empty data directory and a new session key.
"#,
        product = config.product_name,
        identifier = config.identifier,
    )
}

/// `src/main.rs` with the library's real name in it.
pub fn main_rs(project: &Project) -> String {
    MAIN_RS.replace("APP_LIB", &format!("{}_native_lib", project.lib))
}

/// A JSON string literal.
fn json(value: &str) -> String {
    serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string())
}