From 0e583a7c1e45cc79e5d61811c00f3e62a986b064 Mon Sep 17 00:00:00 2001 From: mliem2k Date: Sat, 11 Jul 2026 04:46:52 +0800 Subject: [PATCH] feat: add SolidJS example Add examples/solid, mirroring the existing React/Vue/Svelte example shape (app.zon, hand-owned build.zig, Vite dev server, native dev/run/test) with a SolidJS frontend using createSignal/onMount and solid-js/web's render(). Wired into zig build test-examples-frontends and documented in examples/README.md and the frontend docs page. Closes #57 --- build.zig | 2 + changelog.d/solid-example.md | 1 + docs/src/app/frontend/page.mdx | 1 + examples/README.md | 2 +- examples/solid/README.md | 33 ++ examples/solid/app.zon | 31 ++ examples/solid/build.zig | 511 +++++++++++++++++++++++++ examples/solid/build.zig.zon | 8 + examples/solid/frontend/index.html | 12 + examples/solid/frontend/package.json | 18 + examples/solid/frontend/src/App.tsx | 21 + examples/solid/frontend/src/index.css | 52 +++ examples/solid/frontend/src/main.tsx | 5 + examples/solid/frontend/vite.config.js | 6 + examples/solid/src/main.zig | 46 +++ examples/solid/src/runner.zig | 402 +++++++++++++++++++ 16 files changed, 1150 insertions(+), 1 deletion(-) create mode 100644 changelog.d/solid-example.md create mode 100644 examples/solid/README.md create mode 100644 examples/solid/app.zon create mode 100644 examples/solid/build.zig create mode 100644 examples/solid/build.zig.zon create mode 100644 examples/solid/frontend/index.html create mode 100644 examples/solid/frontend/package.json create mode 100644 examples/solid/frontend/src/App.tsx create mode 100644 examples/solid/frontend/src/index.css create mode 100644 examples/solid/frontend/src/main.tsx create mode 100644 examples/solid/frontend/vite.config.js create mode 100644 examples/solid/src/main.zig create mode 100644 examples/solid/src/runner.zig diff --git a/build.zig b/build.zig index eecf0c0a..619ab5bf 100644 --- a/build.zig +++ b/build.zig @@ -800,11 +800,13 @@ pub fn build(b: *std.Build) void { const frontend_examples_step = b.step("test-examples-frontends", "Run frontend example tests"); addExampleTestStep(b, host_cli_exe, frontend_examples_step, "test-example-next", "Run Next example tests", "examples/next", .owned); addExampleTestStep(b, host_cli_exe, frontend_examples_step, "test-example-react", "Run React example tests", "examples/react", .owned); + addExampleTestStep(b, host_cli_exe, frontend_examples_step, "test-example-solid", "Run Solid example tests", "examples/solid", .owned); addExampleTestStep(b, host_cli_exe, frontend_examples_step, "test-example-svelte", "Run Svelte example tests", "examples/svelte", .owned); addExampleTestStep(b, host_cli_exe, frontend_examples_step, "test-example-vue", "Run Vue example tests", "examples/vue", .owned); addFileContainsCheckStep(b, file_contains_checker, frontend_examples_step, "test-example-frontend-positioning", "Verify frontend example native shell positioning", &.{ .{ .path = "examples/next/README.md", .pattern = "opens the native app shell with WebView content." }, .{ .path = "examples/react/README.md", .pattern = "opens the native app shell with WebView content." }, + .{ .path = "examples/solid/README.md", .pattern = "opens the native app shell with WebView content." }, .{ .path = "examples/svelte/README.md", .pattern = "opens the native app shell with WebView content." }, .{ .path = "examples/vue/README.md", .pattern = "opens the native app shell with WebView content." }, }); diff --git a/changelog.d/solid-example.md b/changelog.d/solid-example.md new file mode 100644 index 00000000..da0b82cd --- /dev/null +++ b/changelog.d/solid-example.md @@ -0,0 +1 @@ +feature: **SolidJS example**: `examples/solid` mirrors the existing React/Vue/Svelte examples (Vite dev server, `zig build run`/`dev`/`test`), for apps that want SolidJS's fine-grained reactivity instead of a virtual DOM. Requested in #57. diff --git a/docs/src/app/frontend/page.mdx b/docs/src/app/frontend/page.mdx index 77efd1ac..34512702 100644 --- a/docs/src/app/frontend/page.mdx +++ b/docs/src/app/frontend/page.mdx @@ -102,6 +102,7 @@ The repository includes complete frontend examples: - `examples/next` - Next.js app with `frontend/out` production assets. - `examples/react` - React app built with Vite. +- `examples/solid` - Solid app built with Vite. - `examples/svelte` - Svelte app built with Vite. - `examples/vue` - Vue app built with Vite. diff --git a/examples/README.md b/examples/README.md index 6a3bc26d..b282a3f4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -40,7 +40,7 @@ native build # produce a ReleaseFast binary in zig-out/bin/ | `native-shell` | Native toolbar/sidebar/statusbar chrome around a WebView content area. | | `native-panels` | Split native panels and stacked native controls around WebView content. | | `browser` | Layered WebViews for isolated page content, engine link flags wired by hand. | -| `next`, `react`, `svelte`, `vue` | Frontend projects with managed install/build/dev-server steps. | +| `next`, `react`, `solid`, `svelte`, `vue` | Frontend projects with managed install/build/dev-server steps. | | `ui-inbox` | The builder-view inbox; its `-Dmobile` lib step feeds the mobile host shims. | | `mobile-canvas` | Builds the mobile embed static library consumed by the iOS/Android canvas shims. | diff --git a/examples/solid/README.md b/examples/solid/README.md new file mode 100644 index 00000000..90118592 --- /dev/null +++ b/examples/solid/README.md @@ -0,0 +1,33 @@ +# Solid Example + +A super basic Native SDK example using SolidJS for the frontend and Zig for the native shell. + +## Run + +```bash +zig build run +``` + +The build installs frontend dependencies, builds the frontend, and opens the native app shell with WebView content. + +## Dev Server + +```bash +zig build dev +``` + +This starts the Solid dev server from `app.zon`, waits for `http://127.0.0.1:5173/`, and launches the native shell with `NATIVE_SDK_FRONTEND_URL`. + +## Frontend + +- Frontend: `solid` +- Production assets: `frontend/dist` +- Dev URL: `http://127.0.0.1:5173/` + +## Using Outside The Repo + +This example references the Native SDK via relative path (`../../`). To use it standalone, override the path: + +```bash +zig build run -Dnative-sdk-path=/path/to/native-sdk +``` diff --git a/examples/solid/app.zon b/examples/solid/app.zon new file mode 100644 index 00000000..2178bcd5 --- /dev/null +++ b/examples/solid/app.zon @@ -0,0 +1,31 @@ +.{ + .id = "dev.native_sdk.solid-example", + .name = "solid-example", + .display_name = "Solid Example", + .version = "0.1.0", + .platforms = .{ "macos", "linux" }, + .permissions = .{}, + .capabilities = .{ "webview" }, + .frontend = .{ + .dist = "frontend/dist", + .entry = "index.html", + .spa_fallback = true, + .dev = .{ + .url = "http://127.0.0.1:5173/", + .command = .{ "npm", "--prefix", "frontend", "run", "dev", "--", "--host", "127.0.0.1" }, + .ready_path = "/", + .timeout_ms = 30000, + }, + }, + .security = .{ + .navigation = .{ + .allowed_origins = .{ "zero://app", "zero://inline", "http://127.0.0.1:5173" }, + .external_links = .{ .action = "deny" }, + }, + }, + .web_engine = "system", + .cef = .{ .dir = "third_party/cef/macos", .auto_install = false }, + .windows = .{ + .{ .label = "main", .title = "Solid Example", .width = 720, .height = 480, .restore_state = true }, + }, +} diff --git a/examples/solid/build.zig b/examples/solid/build.zig new file mode 100644 index 00000000..2079d719 --- /dev/null +++ b/examples/solid/build.zig @@ -0,0 +1,511 @@ +// This example owns its build: it adds the frontend install/build/dev-server steps and the hand-wired WebView shell that the generated app graph does not provide. +const std = @import("std"); + +const PlatformOption = enum { + auto, + null, + macos, + linux, + windows, +}; + +const TraceOption = enum { + off, + events, + runtime, + all, +}; + +const WebEngineOption = enum { + system, + chromium, +}; + +const PackageTarget = enum { + macos, + windows, + linux, +}; + +const default_native_sdk_path = "../.."; +const app_exe_name = "solid"; + +pub fn build(b: *std.Build) void { + const target = nativeSdkTarget(b); + const optimize = b.standardOptimizeOption(.{}); + const platform_option = b.option(PlatformOption, "platform", "Desktop backend: auto, null, macos, linux, windows") orelse .auto; + const trace_option = b.option(TraceOption, "trace", "Trace output: off, events, runtime, all") orelse .events; + const debug_overlay = b.option(bool, "debug-overlay", "Enable debug overlay output") orelse false; + const automation_enabled = b.option(bool, "automation", "Enable Native SDK automation artifacts") orelse false; + const js_bridge_enabled = b.option(bool, "js-bridge", "Enable optional JavaScript bridge stubs") orelse false; + const web_engine_override = b.option(WebEngineOption, "web-engine", "Override app.zon web engine: system, chromium"); + const cef_dir_override = b.option([]const u8, "cef-dir", "Override CEF root directory for Chromium builds"); + const cef_auto_install_override = b.option(bool, "cef-auto-install", "Override app.zon CEF auto-install setting"); + const package_target = b.option(PackageTarget, "package-target", "Package target: macos, windows, linux") orelse .macos; + const native_sdk_path = b.option([]const u8, "native-sdk-path", "Path to the Native SDK framework checkout") orelse default_native_sdk_path; + const optimize_name = @tagName(optimize); + const selected_platform: PlatformOption = switch (platform_option) { + .auto => if (target.result.os.tag == .macos) .macos else if (target.result.os.tag == .linux) .linux else if (target.result.os.tag == .windows) .windows else .null, + else => platform_option, + }; + if (selected_platform == .macos and target.result.os.tag != .macos) { + @panic("-Dplatform=macos requires a macOS target"); + } + if (selected_platform == .linux and target.result.os.tag != .linux) { + @panic("-Dplatform=linux requires a Linux target"); + } + if (selected_platform == .windows and target.result.os.tag != .windows) { + @panic("-Dplatform=windows requires a Windows target"); + } + const app_web_engine = appWebEngineConfig(); + const web_engine = web_engine_override orelse app_web_engine.web_engine; + const cef_dir = cef_dir_override orelse defaultCefDir(selected_platform, app_web_engine.cef_dir); + const cef_auto_install = cef_auto_install_override orelse app_web_engine.cef_auto_install; + if (web_engine == .chromium and selected_platform != .macos) { + @panic("-Dweb-engine=chromium currently requires -Dplatform=macos"); + } + + const native_sdk_mod = nativeSdkModule(b, target, optimize, native_sdk_path); + const options = b.addOptions(); + options.addOption([]const u8, "platform", switch (selected_platform) { + .auto => unreachable, + .null => "null", + .macos => "macos", + .linux => "linux", + .windows => "windows", + }); + options.addOption([]const u8, "trace", @tagName(trace_option)); + options.addOption([]const u8, "web_engine", @tagName(web_engine)); + options.addOption(bool, "debug_overlay", debug_overlay); + options.addOption(bool, "automation", automation_enabled); + options.addOption(bool, "js_bridge", js_bridge_enabled); + const options_mod = options.createModule(); + + const runner_mod = localModule(b, target, optimize, "src/runner.zig"); + runner_mod.addImport("native_sdk", native_sdk_mod); + runner_mod.addImport("build_options", options_mod); + runner_mod.addImport("app_manifest_zon", b.createModule(.{ .root_source_file = b.path("app.zon") })); + + const app_mod = localModule(b, target, optimize, "src/main.zig"); + app_mod.addImport("native_sdk", native_sdk_mod); + app_mod.addImport("runner", runner_mod); + const exe = b.addExecutable(.{ + .name = app_exe_name, + .root_module = app_mod, + }); + linkPlatform(b, target, app_mod, exe, selected_platform, web_engine, native_sdk_path, cef_dir, cef_auto_install); + b.installArtifact(exe); + + const frontend_install = b.addSystemCommand(&.{ "npm", "install", "--prefix", "frontend" }); + const frontend_install_step = b.step("frontend-install", "Install frontend dependencies"); + frontend_install_step.dependOn(&frontend_install.step); + + const frontend_build = b.addSystemCommand(&.{ "npm", "--prefix", "frontend", "run", "build" }); + frontend_build.step.dependOn(&frontend_install.step); + const frontend_step = b.step("frontend-build", "Build the frontend"); + frontend_step.dependOn(&frontend_build.step); + + const run = b.addRunArtifact(exe); + run.step.dependOn(&frontend_build.step); + addCefRuntimeRunFiles(b, target, run, exe, web_engine, cef_dir); + addWebView2RuntimeRunFiles(b, target, run, web_engine, native_sdk_path); + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run.step); + + const dev = b.addSystemCommand(&.{ "native", "dev", "--manifest", "app.zon", "--binary" }); + dev.addFileArg(exe.getEmittedBin()); + dev.step.dependOn(&exe.step); + dev.step.dependOn(&frontend_install.step); + const dev_step = b.step("dev", "Run the frontend dev server and native shell"); + dev_step.dependOn(&dev.step); + + const package = b.addSystemCommand(&.{ + "native", + "package", + "--target", + @tagName(package_target), + "--manifest", + "app.zon", + "--assets", + "frontend/dist", + "--optimize", + optimize_name, + "--output", + b.fmt("zig-out/package/{s}-0.1.0-{s}-{s}{s}", .{ app_exe_name, @tagName(package_target), optimize_name, packageSuffix(package_target) }), + "--binary", + }); + package.addFileArg(exe.getEmittedBin()); + package.addArgs(&.{ "--web-engine", @tagName(web_engine), "--cef-dir", cef_dir }); + if (cef_auto_install) package.addArg("--cef-auto-install"); + package.step.dependOn(&exe.step); + package.step.dependOn(&frontend_build.step); + const package_step = b.step("package", "Create a local package artifact"); + package_step.dependOn(&package.step); + + const tests = b.addTest(.{ .root_module = app_mod }); + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&b.addRunArtifact(tests).step); +} + +fn nativeSdkTarget(b: *std.Build) std.Build.ResolvedTarget { + const target = b.standardTargetOptions(.{}); + if (target.result.os.tag != .macos) return target; + + if (b.sysroot == null) { + b.sysroot = macosSdkPath(b) orelse b.sysroot; + } + + var query = target.query; + query.os_tag = .macos; + query.os_version_min = .{ .semver = .{ .major = 11, .minor = 0, .patch = 0 } }; + return b.resolveTargetQuery(query); +} + +fn macosSdkPath(b: *std.Build) ?[]const u8 { + if (b.graph.environ_map.get("SDKROOT")) |sdkroot| { + if (sdkroot.len > 0) return sdkroot; + } + + const result = std.process.run(b.allocator, b.graph.io, .{ + .argv = &.{ "xcrun", "--sdk", "macosx", "--show-sdk-path" }, + .stdout_limit = .limited(4096), + .stderr_limit = .limited(4096), + }) catch return null; + defer b.allocator.free(result.stderr); + if (result.term != .exited or result.term.exited != 0) { + b.allocator.free(result.stdout); + return null; + } + return std.mem.trimEnd(u8, result.stdout, "\r\n"); +} + +fn localModule(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, path: []const u8) *std.Build.Module { + return b.createModule(.{ + .root_source_file = b.path(path), + .target = target, + .optimize = optimize, + }); +} + +fn nativeSdkPath(b: *std.Build, native_sdk_path: []const u8, sub_path: []const u8) std.Build.LazyPath { + return .{ .cwd_relative = b.pathJoin(&.{ native_sdk_path, sub_path }) }; +} + +fn nativeSdkModule(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, native_sdk_path: []const u8) *std.Build.Module { + const geometry_mod = externalModule(b, target, optimize, native_sdk_path, "src/primitives/geometry/root.zig"); + const assets_mod = externalModule(b, target, optimize, native_sdk_path, "src/primitives/assets/root.zig"); + const app_dirs_mod = externalModule(b, target, optimize, native_sdk_path, "src/primitives/app_dirs/root.zig"); + const trace_mod = externalModule(b, target, optimize, native_sdk_path, "src/primitives/trace/root.zig"); + const app_manifest_mod = externalModule(b, target, optimize, native_sdk_path, "src/primitives/app_manifest/root.zig"); + const diagnostics_mod = externalModule(b, target, optimize, native_sdk_path, "src/primitives/diagnostics/root.zig"); + const platform_info_mod = externalModule(b, target, optimize, native_sdk_path, "src/primitives/platform_info/root.zig"); + const json_mod = externalModule(b, target, optimize, native_sdk_path, "src/primitives/json/root.zig"); + const canvas_mod = externalModule(b, target, optimize, native_sdk_path, "src/primitives/canvas/root.zig"); + canvas_mod.addImport("geometry", geometry_mod); + canvas_mod.addImport("json", json_mod); + const debug_mod = externalModule(b, target, optimize, native_sdk_path, "src/debug/root.zig"); + debug_mod.addImport("app_dirs", app_dirs_mod); + debug_mod.addImport("trace", trace_mod); + + const native_sdk_mod = externalModule(b, target, optimize, native_sdk_path, "src/root.zig"); + native_sdk_mod.addImport("geometry", geometry_mod); + native_sdk_mod.addImport("assets", assets_mod); + native_sdk_mod.addImport("app_dirs", app_dirs_mod); + native_sdk_mod.addImport("trace", trace_mod); + native_sdk_mod.addImport("app_manifest", app_manifest_mod); + native_sdk_mod.addImport("diagnostics", diagnostics_mod); + native_sdk_mod.addImport("platform_info", platform_info_mod); + native_sdk_mod.addImport("json", json_mod); + native_sdk_mod.addImport("canvas", canvas_mod); + return native_sdk_mod; +} + +fn externalModule(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, native_sdk_path: []const u8, path: []const u8) *std.Build.Module { + return b.createModule(.{ + .root_source_file = nativeSdkPath(b, native_sdk_path, path), + .target = target, + .optimize = optimize, + }); +} + +fn linkPlatform(b: *std.Build, target: std.Build.ResolvedTarget, app_mod: *std.Build.Module, exe: *std.Build.Step.Compile, platform: PlatformOption, web_engine: WebEngineOption, native_sdk_path: []const u8, cef_dir: []const u8, cef_auto_install: bool) void { + if (platform == .macos) { + switch (web_engine) { + .system => { + const sdk_include = if (b.sysroot) |sysroot| b.fmt("-I{s}/usr/include", .{sysroot}) else ""; + const flags: []const []const u8 = if (b.sysroot) |sysroot| &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC", "-mmacosx-version-min=11.0", "-isysroot", sysroot, sdk_include } else &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC", "-mmacosx-version-min=11.0" }; + app_mod.addCSourceFile(.{ .file = nativeSdkPath(b, native_sdk_path, "src/platform/macos/appkit_host.m"), .flags = flags }); + app_mod.linkFramework("WebKit", .{}); + }, + .chromium => { + const cef_check = addCefCheck(b, target, cef_dir); + if (cef_auto_install) { + const cef_auto = b.addSystemCommand(&.{ "native", "cef", "install", "--dir", cef_dir }); + cef_check.step.dependOn(&cef_auto.step); + } + exe.step.dependOn(&cef_check.step); + const include_arg = b.fmt("-I{s}", .{cef_dir}); + const define_arg = b.fmt("-DNATIVE_SDK_CEF_DIR=\"{s}\"", .{cef_dir}); + // The SDK's usr/include must stay a system include dir (searched after zig's + // bundled libc++/libc headers). A plain -I shadows libc++'s / + // wrappers in ObjC++ and surfaces SDK nullability gaps as a diagnostic flood. + const sdk_include = if (b.sysroot) |sysroot| b.fmt("-isystem{s}/usr/include", .{sysroot}) else ""; + const flags: []const []const u8 = if (b.sysroot) |sysroot| &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC++", "-std=c++17", "-stdlib=libc++", "-mmacosx-version-min=11.0", "-isysroot", sysroot, sdk_include, include_arg, define_arg } else &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC++", "-std=c++17", "-stdlib=libc++", "-mmacosx-version-min=11.0", include_arg, define_arg }; + app_mod.addCSourceFile(.{ .file = nativeSdkPath(b, native_sdk_path, "src/platform/macos/cef_host.mm"), .flags = flags }); + app_mod.addObjectFile(b.path(b.fmt("{s}/libcef_dll_wrapper/libcef_dll_wrapper.a", .{cef_dir}))); + app_mod.addFrameworkPath(b.path(b.fmt("{s}/Release", .{cef_dir}))); + app_mod.linkFramework("Chromium Embedded Framework", .{}); + app_mod.addRPath(.{ .cwd_relative = "@executable_path/Frameworks" }); + }, + } + if (b.sysroot) |sysroot| { + app_mod.addFrameworkPath(.{ .cwd_relative = b.pathJoin(&.{ sysroot, "System/Library/Frameworks" }) }); + } + app_mod.linkFramework("AppKit", .{}); + // The audio playback service (the AppKit host's single AVPlayer). + app_mod.linkFramework("AVFoundation", .{}); + // Spectrum analysis of the app's own playback: the MediaToolbox + // audio tap hands the player's PCM to the host, and Accelerate + // (vDSP) turns it into band magnitudes. + app_mod.linkFramework("MediaToolbox", .{}); + app_mod.linkFramework("Accelerate", .{}); + app_mod.linkFramework("Foundation", .{}); + app_mod.linkFramework("CoreText", .{}); + app_mod.linkFramework("UniformTypeIdentifiers", .{}); + app_mod.linkFramework("Security", .{}); + app_mod.linkFramework("Metal", .{}); + app_mod.linkFramework("QuartzCore", .{}); + app_mod.linkSystemLibrary("c", .{}); + if (web_engine == .chromium) app_mod.linkSystemLibrary("c++", .{}); + } else if (platform == .linux) { + switch (web_engine) { + .system => { + app_mod.addCSourceFile(.{ .file = nativeSdkPath(b, native_sdk_path, "src/platform/linux/gtk_host.c"), .flags = &.{} }); + app_mod.linkSystemLibrary("gtk4", .{}); + app_mod.linkSystemLibrary("webkitgtk-6.0", .{}); + app_mod.linkSystemLibrary("dl", .{}); + }, + .chromium => { + const cef_check = addCefCheck(b, target, cef_dir); + if (cef_auto_install) { + const cef_auto = b.addSystemCommand(&.{ "native", "cef", "install", "--dir", cef_dir }); + cef_check.step.dependOn(&cef_auto.step); + } + exe.step.dependOn(&cef_check.step); + const include_arg = b.fmt("-I{s}", .{cef_dir}); + const define_arg = b.fmt("-DNATIVE_SDK_CEF_DIR=\"{s}\"", .{cef_dir}); + app_mod.addCSourceFile(.{ .file = nativeSdkPath(b, native_sdk_path, "src/platform/linux/cef_host.cpp"), .flags = &.{ "-std=c++17", include_arg, define_arg } }); + app_mod.addObjectFile(b.path(b.fmt("{s}/libcef_dll_wrapper/libcef_dll_wrapper.a", .{cef_dir}))); + app_mod.addLibraryPath(b.path(b.fmt("{s}/Release", .{cef_dir}))); + app_mod.linkSystemLibrary("cef", .{}); + app_mod.addRPath(.{ .cwd_relative = "$ORIGIN" }); + }, + } + app_mod.linkSystemLibrary("c", .{}); + if (web_engine == .chromium) app_mod.linkSystemLibrary("stdc++", .{}); + } else if (platform == .windows) { + switch (web_engine) { + .system => { + // The vendored WebView2 SDK header (third_party/webview2) + // turns on the host's embedded-WebView layer; the host + // fails the compile by design if it cannot be found. + app_mod.addIncludePath(nativeSdkPath(b, native_sdk_path, "third_party/webview2/include")); + app_mod.addCSourceFile(.{ .file = nativeSdkPath(b, native_sdk_path, "src/platform/windows/webview2_host.cpp"), .flags = &.{"-std=c++17"} }); + // WebView2Loader.dll rides next to the installed app + // executable: the host loads it at runtime to discover + // the machine's WebView2 runtime. Canvas apps never + // touch it. + const loader = b.addInstallBinFile(nativeSdkPath(b, native_sdk_path, webView2LoaderSubPath(target)), "WebView2Loader.dll"); + b.getInstallStep().dependOn(&loader.step); + }, + .chromium => { + const cef_check = addCefCheck(b, target, cef_dir); + if (cef_auto_install) { + const cef_auto = b.addSystemCommand(&.{ "native", "cef", "install", "--dir", cef_dir }); + cef_check.step.dependOn(&cef_auto.step); + } + exe.step.dependOn(&cef_check.step); + const include_arg = b.fmt("-I{s}", .{cef_dir}); + const define_arg = b.fmt("-DNATIVE_SDK_CEF_DIR=\"{s}\"", .{cef_dir}); + app_mod.addCSourceFile(.{ .file = nativeSdkPath(b, native_sdk_path, "src/platform/windows/cef_host.cpp"), .flags = &.{ "-std=c++17", include_arg, define_arg } }); + app_mod.addObjectFile(b.path(b.fmt("{s}/libcef_dll_wrapper/libcef_dll_wrapper.lib", .{cef_dir}))); + app_mod.addLibraryPath(b.path(b.fmt("{s}/Release", .{cef_dir}))); + }, + } + app_mod.linkSystemLibrary("c", .{}); + app_mod.linkSystemLibrary("c++", .{}); + app_mod.linkSystemLibrary("user32", .{}); + app_mod.linkSystemLibrary("gdi32", .{}); + app_mod.linkSystemLibrary("imm32", .{}); + app_mod.linkSystemLibrary("comctl32", .{}); + app_mod.linkSystemLibrary("ole32", .{}); + app_mod.linkSystemLibrary("oleacc", .{}); + app_mod.linkSystemLibrary("shell32", .{}); + // The audio backend: Media Foundation (session + source resolver + // + streaming audio renderer) and WinHTTP (the cache fill). + app_mod.linkSystemLibrary("mf", .{}); + app_mod.linkSystemLibrary("mfplat", .{}); + app_mod.linkSystemLibrary("winhttp", .{}); + if (web_engine == .chromium) app_mod.linkSystemLibrary("libcef", .{}); + } +} + +/// The vendored WebView2Loader.dll for the target architecture, relative +/// to the framework root. +fn webView2LoaderSubPath(target: std.Build.ResolvedTarget) []const u8 { + return if (target.result.cpu.arch == .aarch64) + "third_party/webview2/arm64/WebView2Loader.dll" + else + "third_party/webview2/x64/WebView2Loader.dll"; +} + +/// `zig build run` executes the cached artifact, which has no installed +/// WebView2Loader.dll beside it; the vendored loader's directory goes on +/// the run step's PATH so the host's LoadLibrary resolves it in dev runs. +fn addWebView2RuntimeRunFiles(b: *std.Build, target: std.Build.ResolvedTarget, run: *std.Build.Step.Run, web_engine: WebEngineOption, native_sdk_path: []const u8) void { + if (web_engine != .system) return; + if (target.result.os.tag != .windows) return; + const loader_dir = std.fs.path.dirname(webView2LoaderSubPath(target)).?; + run.addPathDir(b.pathFromRoot(b.pathJoin(&.{ native_sdk_path, loader_dir }))); +} + +fn addCefRuntimeRunFiles(b: *std.Build, target: std.Build.ResolvedTarget, run: *std.Build.Step.Run, exe: *std.Build.Step.Compile, web_engine: WebEngineOption, cef_dir: []const u8) void { + if (web_engine != .chromium) return; + if (target.result.os.tag != .macos) return; + const copy = b.addSystemCommand(&.{ + "sh", "-c", + b.fmt( + \\set -e + \\exe="$0" + \\exe_dir="$(dirname "$exe")" + \\rm -rf "zig-out/Frameworks/Chromium Embedded Framework.framework" "zig-out/bin/Frameworks/Chromium Embedded Framework.framework" ".zig-cache/o/Frameworks/Chromium Embedded Framework.framework" && + \\mkdir -p "zig-out/Frameworks" "zig-out/bin/Frameworks" ".zig-cache/o/Frameworks" "$exe_dir" && + \\cp -R "{s}/Release/Chromium Embedded Framework.framework" "zig-out/Frameworks/" && + \\cp -R "{s}/Release/Chromium Embedded Framework.framework" "zig-out/bin/Frameworks/" && + \\cp -R "{s}/Release/Chromium Embedded Framework.framework" ".zig-cache/o/Frameworks/" && + \\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/libEGL.dylib" "$exe_dir/" && + \\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/libGLESv2.dylib" "$exe_dir/" && + \\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/libvk_swiftshader.dylib" "$exe_dir/" && + \\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/vk_swiftshader_icd.json" "$exe_dir/" + , .{ cef_dir, cef_dir, cef_dir, cef_dir, cef_dir, cef_dir, cef_dir }), + }); + copy.addFileArg(exe.getEmittedBin()); + run.step.dependOn(©.step); +} + +fn addCefCheck(b: *std.Build, target: std.Build.ResolvedTarget, cef_dir: []const u8) *std.Build.Step.Run { + const script = switch (target.result.os.tag) { + .macos => b.fmt( + \\test -f "{s}/include/cef_app.h" && + \\test -d "{s}/Release/Chromium Embedded Framework.framework" && + \\test -f "{s}/libcef_dll_wrapper/libcef_dll_wrapper.a" || {{ + \\ echo "missing CEF dependency for -Dweb-engine=chromium" >&2 + \\ echo "Expected:" >&2 + \\ echo " {s}/include/cef_app.h" >&2 + \\ echo " {s}/Release/Chromium Embedded Framework.framework" >&2 + \\ echo " {s}/libcef_dll_wrapper/libcef_dll_wrapper.a" >&2 + \\ echo "Fix with: native cef install --dir {s}" >&2 + \\ echo "Or rerun with: -Dcef-auto-install=true" >&2 + \\ echo "Pass -Dcef-dir=/path/to/cef if your bundle lives elsewhere." >&2 + \\ exit 1 + \\}} + , .{ cef_dir, cef_dir, cef_dir, cef_dir, cef_dir, cef_dir, cef_dir }), + .linux => b.fmt( + \\test -f "{s}/include/cef_app.h" && + \\test -f "{s}/Release/libcef.so" && + \\test -f "{s}/libcef_dll_wrapper/libcef_dll_wrapper.a" || {{ + \\ echo "missing CEF dependency for -Dweb-engine=chromium" >&2 + \\ echo "Fix with: native cef install --dir {s}" >&2 + \\ exit 1 + \\}} + , .{ cef_dir, cef_dir, cef_dir, cef_dir }), + .windows => b.fmt( + \\test -f "{s}/include/cef_app.h" && + \\test -f "{s}/Release/libcef.dll" && + \\test -f "{s}/libcef_dll_wrapper/libcef_dll_wrapper.lib" || {{ + \\ echo "missing CEF dependency for -Dweb-engine=chromium" >&2 + \\ echo "Fix with: native cef install --dir {s}" >&2 + \\ exit 1 + \\}} + , .{ cef_dir, cef_dir, cef_dir, cef_dir }), + else => "echo unsupported CEF target >&2; exit 1", + }; + return b.addSystemCommand(&.{ "sh", "-c", script }); +} + +fn packageSuffix(target: PackageTarget) []const u8 { + return switch (target) { + .macos => ".app", + .windows, .linux => "", + }; +} + +const AppWebEngineConfig = struct { + web_engine: WebEngineOption = .system, + cef_dir: []const u8 = "third_party/cef/macos", + cef_auto_install: bool = false, +}; + +fn defaultCefDir(platform: PlatformOption, configured: []const u8) []const u8 { + if (!std.mem.eql(u8, configured, "third_party/cef/macos")) return configured; + return switch (platform) { + .linux => "third_party/cef/linux", + .windows => "third_party/cef/windows", + else => configured, + }; +} + +fn appWebEngineConfig() AppWebEngineConfig { + const source = @embedFile("app.zon"); + var config: AppWebEngineConfig = .{}; + if (stringField(source, ".web_engine")) |value| { + config.web_engine = parseWebEngine(value) orelse .system; + } + if (objectSection(source, ".cef")) |cef| { + if (stringField(cef, ".dir")) |value| config.cef_dir = value; + if (boolField(cef, ".auto_install")) |value| config.cef_auto_install = value; + } + return config; +} + +fn parseWebEngine(value: []const u8) ?WebEngineOption { + if (std.mem.eql(u8, value, "system")) return .system; + if (std.mem.eql(u8, value, "chromium")) return .chromium; + return null; +} + +fn stringField(source: []const u8, field: []const u8) ?[]const u8 { + const field_index = std.mem.indexOf(u8, source, field) orelse return null; + const equals = std.mem.indexOfScalarPos(u8, source, field_index, '=') orelse return null; + const start_quote = std.mem.indexOfScalarPos(u8, source, equals, '"') orelse return null; + const end_quote = std.mem.indexOfScalarPos(u8, source, start_quote + 1, '"') orelse return null; + return source[start_quote + 1 .. end_quote]; +} + +fn objectSection(source: []const u8, field: []const u8) ?[]const u8 { + const field_index = std.mem.indexOf(u8, source, field) orelse return null; + const open = std.mem.indexOfScalarPos(u8, source, field_index, '{') orelse return null; + var depth: usize = 0; + var index = open; + while (index < source.len) : (index += 1) { + switch (source[index]) { + '{' => depth += 1, + '}' => { + depth -= 1; + if (depth == 0) return source[open + 1 .. index]; + }, + else => {}, + } + } + return null; +} + +fn boolField(source: []const u8, field: []const u8) ?bool { + const field_index = std.mem.indexOf(u8, source, field) orelse return null; + const equals = std.mem.indexOfScalarPos(u8, source, field_index, '=') orelse return null; + var index = equals + 1; + while (index < source.len and std.ascii.isWhitespace(source[index])) : (index += 1) {} + if (std.mem.startsWith(u8, source[index..], "true")) return true; + if (std.mem.startsWith(u8, source[index..], "false")) return false; + return null; +} diff --git a/examples/solid/build.zig.zon b/examples/solid/build.zig.zon new file mode 100644 index 00000000..e9f18d5c --- /dev/null +++ b/examples/solid/build.zig.zon @@ -0,0 +1,8 @@ +.{ + .name = .solid, + .fingerprint = 0xa438cdbc5a707070, + .version = "0.1.0", + .minimum_zig_version = "0.16.0", + .dependencies = .{}, + .paths = .{ "build.zig", "build.zig.zon", "src", "assets", "frontend", "app.zon", "README.md" }, +} diff --git a/examples/solid/frontend/index.html b/examples/solid/frontend/index.html new file mode 100644 index 00000000..402afdf8 --- /dev/null +++ b/examples/solid/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Solid + + +
+ + + diff --git a/examples/solid/frontend/package.json b/examples/solid/frontend/package.json new file mode 100644 index 00000000..4aad97f2 --- /dev/null +++ b/examples/solid/frontend/package.json @@ -0,0 +1,18 @@ +{ + "name": "solid", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "solid-js": "^1.9.14" + }, + "devDependencies": { + "vite": "^8.0.11", + "vite-plugin-solid": "^2.11.12" + } +} diff --git a/examples/solid/frontend/src/App.tsx b/examples/solid/frontend/src/App.tsx new file mode 100644 index 00000000..aa83cea7 --- /dev/null +++ b/examples/solid/frontend/src/App.tsx @@ -0,0 +1,21 @@ +import { createSignal, onMount } from "solid-js"; + +export default function App() { + const [bridge, setBridge] = createSignal("checking..."); + + onMount(() => { + setBridge((window as any).zero ? "available" : "not enabled"); + }); + + return ( +
+

Native SDK + Solid

+

Solid

+

A Solid frontend running inside the system WebView.

+
+ Native bridge + {bridge()} +
+
+ ); +} diff --git a/examples/solid/frontend/src/index.css b/examples/solid/frontend/src/index.css new file mode 100644 index 00000000..0f107fc5 --- /dev/null +++ b/examples/solid/frontend/src/index.css @@ -0,0 +1,52 @@ +:root { + color: #0f172a; + background: #f8fafc; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +body { + min-width: 320px; + min-height: 100vh; + margin: 0; + display: grid; + place-items: center; +} + +main { + width: min(560px, calc(100vw - 48px)); + padding: 32px; + border-radius: 24px; + background: white; + box-shadow: 0 24px 60px rgba(15, 23, 42, 0.14); +} + +h1 { + margin: 0 0 12px; + font-size: clamp(2rem, 8vw, 4rem); + line-height: 1; +} + +.eyebrow { + margin: 0 0 12px; + color: #2563eb; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.lede { + margin: 0 0 24px; + color: #475569; + line-height: 1.6; +} + +.card { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 16px; + border: 1px solid #e2e8f0; + border-radius: 16px; + background: #f8fafc; +} diff --git a/examples/solid/frontend/src/main.tsx b/examples/solid/frontend/src/main.tsx new file mode 100644 index 00000000..3ccf2db1 --- /dev/null +++ b/examples/solid/frontend/src/main.tsx @@ -0,0 +1,5 @@ +import { render } from "solid-js/web"; +import App from "./App"; +import "./index.css"; + +render(() => , document.getElementById("root")!); diff --git a/examples/solid/frontend/vite.config.js b/examples/solid/frontend/vite.config.js new file mode 100644 index 00000000..4a303c3c --- /dev/null +++ b/examples/solid/frontend/vite.config.js @@ -0,0 +1,6 @@ +import { defineConfig } from "vite"; +import solid from "vite-plugin-solid"; + +export default defineConfig({ + plugins: [solid()], +}); diff --git a/examples/solid/src/main.zig b/examples/solid/src/main.zig new file mode 100644 index 00000000..c31c6a16 --- /dev/null +++ b/examples/solid/src/main.zig @@ -0,0 +1,46 @@ +const std = @import("std"); +const runner = @import("runner"); +const native_sdk = @import("native_sdk"); + +pub const panic = std.debug.FullPanic(native_sdk.debug.capturePanic); + +const App = struct { + env_map: *std.process.Environ.Map, + + fn app(self: *@This()) native_sdk.App { + return .{ + .context = self, + .name = "solid-example", + .source = native_sdk.frontend.productionSource(.{ .dist = "frontend/dist" }), + .source_fn = source, + }; + } + + fn source(context: *anyopaque) anyerror!native_sdk.WebViewSource { + const self: *@This() = @ptrCast(@alignCast(context)); + return native_sdk.frontend.sourceFromEnv(self.env_map, .{ + .dist = "frontend/dist", + .entry = "index.html", + }); + } +}; + +const dev_origins = [_][]const u8{ "zero://app", "zero://inline", "http://127.0.0.1:5173" }; + +pub fn main(init: std.process.Init) !void { + var app = App{ .env_map = init.environ_map }; + try runner.runWithOptions(app.app(), .{ + .app_name = "Solid Example", + .window_title = "Solid Example", + .bundle_id = "dev.native_sdk.solid-example", + .security = .{ + .navigation = .{ .allowed_origins = &dev_origins }, + }, + }, init); +} + +test "production source points at Solid build output" { + const source = native_sdk.frontend.productionSource(.{ .dist = "frontend/dist" }); + try std.testing.expectEqual(native_sdk.WebViewSourceKind.assets, source.kind); + try std.testing.expectEqualStrings("frontend/dist", source.asset_options.?.root_path); +} diff --git a/examples/solid/src/runner.zig b/examples/solid/src/runner.zig new file mode 100644 index 00000000..2d19531f --- /dev/null +++ b/examples/solid/src/runner.zig @@ -0,0 +1,402 @@ +const std = @import("std"); +const build_options = @import("build_options"); +const native_sdk = @import("native_sdk"); +const app_manifest = @import("app_manifest_zon"); +const manifest_shortcuts = if (@hasField(@TypeOf(app_manifest), "shortcuts")) app_manifest.shortcuts else .{}; +const manifest_windows = if (@hasField(@TypeOf(app_manifest), "windows")) app_manifest.windows else .{}; + +pub const StdoutTraceSink = struct { + pub fn sink(self: *StdoutTraceSink) native_sdk.trace.Sink { + return .{ .context = self, .write_fn = write }; + } + + fn write(context: *anyopaque, record: native_sdk.trace.Record) native_sdk.trace.WriteError!void { + _ = context; + if (!shouldTrace(record)) return; + // Never fail on an oversized record: logging failures must + // degrade (truncated output), not fail dispatch upstream. + var buffer: [4096]u8 = undefined; + std.debug.print("{s}\n", .{native_sdk.trace.formatTextBounded(record, &buffer)}); + } +}; + +pub const RunOptions = struct { + app_name: []const u8, + window_title: []const u8 = "", + bundle_id: []const u8, + icon_path: []const u8 = "assets/icon.png", + bridge: ?native_sdk.BridgeDispatcher = null, + builtin_bridge: native_sdk.BridgePolicy = .{}, + security: native_sdk.SecurityPolicy = .{}, + shortcuts: ?[]const native_sdk.Shortcut = null, + + fn appInfo(self: RunOptions, buffers: *StateBuffers) native_sdk.AppInfo { + var info: native_sdk.AppInfo = .{ + .app_name = self.app_name, + // The identity the OS shows (application menu, Dock, About + // panel) reads straight from app.zon at comptime, so dev + // runs carry the same display name and version a packaged + // bundle gets from its Info.plist. + .display_name = manifestStringField("display_name"), + .version = manifestStringField("version"), + .description = manifestStringField("description"), + .has_web_content = manifestHasWebContent(), + .window_title = self.window_title, + .bundle_id = self.bundle_id, + .icon_path = self.icon_path, + }; + const windows = manifestWindowOptions(buffers); + if (windows.len > 0) { + info.main_window = windows[0]; + info.windows = windows; + } + return info; + } + + fn resolvedShortcuts(self: RunOptions, storage: *ShortcutStorage) []const native_sdk.Shortcut { + return self.shortcuts orelse storage.fromManifest(); + } +}; + +const ShortcutStorage = struct { + shortcuts: [native_sdk.platform.max_shortcuts]native_sdk.Shortcut = undefined, + + fn fromManifest(self: *ShortcutStorage) []const native_sdk.Shortcut { + comptime { + if (manifest_shortcuts.len > native_sdk.platform.max_shortcuts) { + @compileError("app.zon defines too many shortcuts"); + } + } + + inline for (manifest_shortcuts, 0..) |shortcut, index| { + self.shortcuts[index] = .{ + .id = shortcut.id, + .key = shortcut.key, + .modifiers = shortcutModifiers(shortcut), + }; + } + return self.shortcuts[0..manifest_shortcuts.len]; + } +}; + +/// A top-level app.zon string field (`display_name`, `version`, +/// `description`), or "" when the manifest omits it — optional identity +/// stays optional all the way into `AppInfo`. +fn manifestStringField(comptime field: []const u8) []const u8 { + if (comptime !@hasField(@TypeOf(app_manifest), field)) return ""; + const value = @field(app_manifest, field); + if (comptime @TypeOf(value) == @TypeOf(null)) return ""; + return value; +} + +/// Whether app.zon declares web content: the `webview` capability or a +/// `frontend` block. Hosts build honest default menus from this — web +/// items like Reload only exist when a webview can answer them, so +/// canvas-only apps never ship dead menu items. +fn manifestHasWebContent() bool { + if (comptime @hasField(@TypeOf(app_manifest), "frontend")) return true; + if (comptime !@hasField(@TypeOf(app_manifest), "capabilities")) return false; + inline for (app_manifest.capabilities) |capability| { + if (comptime std.mem.eql(u8, capability, "webview")) return true; + } + return false; +} + +fn manifestWindowOptions(buffers: *StateBuffers) []const native_sdk.WindowOptions { + comptime { + if (manifest_windows.len > native_sdk.platform.max_windows) { + @compileError("app.zon defines too many windows"); + } + } + + inline for (manifest_windows, 0..) |window, index| { + buffers.restored_windows[index] = manifestWindow(window, index); + } + return buffers.restored_windows[0..manifest_windows.len]; +} + +fn manifestWindow(comptime window: anytype, comptime index: usize) native_sdk.WindowOptions { + return .{ + .id = index + 1, + .label = windowLabel(window, index), + .title = windowTitle(window), + .default_frame = native_sdk.geometry.RectF.init( + windowFloat(window, "x", 0), + windowFloat(window, "y", 0), + windowFloat(window, "width", 720), + windowFloat(window, "height", 480), + ), + .resizable = windowBool(window, "resizable", true), + .restore_state = windowBool(window, "restore_state", true), + .restore_policy = windowRestorePolicy(window), + }; +} + +fn windowLabel(comptime window: anytype, comptime index: usize) []const u8 { + if (comptime @hasField(@TypeOf(window), "label")) return window.label; + return if (index == 0) "main" else "window"; +} + +fn windowTitle(comptime window: anytype) []const u8 { + if (comptime !@hasField(@TypeOf(window), "title")) return ""; + const title = window.title; + if (comptime @TypeOf(title) == @TypeOf(null)) return ""; + return title; +} + +fn windowFloat(comptime window: anytype, comptime field: []const u8, comptime default_value: f32) f32 { + if (comptime @hasField(@TypeOf(window), field)) return @field(window, field); + return default_value; +} + +fn windowBool(comptime window: anytype, comptime field: []const u8, comptime default_value: bool) bool { + if (comptime @hasField(@TypeOf(window), field)) return @field(window, field); + return default_value; +} + +fn windowRestorePolicy(comptime window: anytype) native_sdk.WindowRestorePolicy { + if (comptime !@hasField(@TypeOf(window), "restore_policy")) return .clamp_to_visible_screen; + const value = window.restore_policy; + if (comptime std.mem.eql(u8, value, "clamp_to_visible_screen")) return .clamp_to_visible_screen; + if (comptime std.mem.eql(u8, value, "center_on_primary")) return .center_on_primary; + @compileError("unknown app.zon window restore_policy"); +} + +fn shortcutModifiers(comptime shortcut: anytype) native_sdk.ShortcutModifiers { + const values = if (@hasField(@TypeOf(shortcut), "modifiers")) shortcut.modifiers else .{}; + var modifiers: native_sdk.ShortcutModifiers = .{}; + inline for (values) |value| { + const modifier: []const u8 = value; + if (comptime std.mem.eql(u8, modifier, "primary")) { + modifiers.primary = true; + } else if (comptime std.mem.eql(u8, modifier, "command")) { + modifiers.command = true; + } else if (comptime std.mem.eql(u8, modifier, "control")) { + modifiers.control = true; + } else if (comptime std.mem.eql(u8, modifier, "option") or std.mem.eql(u8, modifier, "alt")) { + modifiers.option = true; + } else if (comptime std.mem.eql(u8, modifier, "shift")) { + modifiers.shift = true; + } else { + @compileError("unknown app.zon shortcut modifier"); + } + } + return modifiers; +} + +pub fn runWithOptions(app: native_sdk.App, options: RunOptions, init: std.process.Init) !void { + if (build_options.debug_overlay) { + std.debug.print("debug-overlay=true backend={s} web-engine={s} trace={s}\n", .{ build_options.platform, build_options.web_engine, build_options.trace }); + } + if (comptime std.mem.eql(u8, build_options.platform, "macos")) { + try runMacos(app, options, init); + } else if (comptime std.mem.eql(u8, build_options.platform, "linux")) { + try runLinux(app, options, init); + } else if (comptime std.mem.eql(u8, build_options.platform, "windows")) { + try runWindows(app, options, init); + } else { + try runNull(app, options, init); + } +} + +fn runNull(app: native_sdk.App, options: RunOptions, init: std.process.Init) !void { + var buffers: StateBuffers = undefined; + var app_info = options.appInfo(&buffers); + const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers); + var null_platform = native_sdk.NullPlatform.initWithOptions(.{}, webEngine(), app_info); + var trace_sink = StdoutTraceSink{}; + var log_buffers: native_sdk.debug.LogPathBuffers = .{}; + const log_setup = native_sdk.debug.setupLogging(init.io, init.environ_map, app_info.bundle_id, &log_buffers) catch null; + if (log_setup) |setup| native_sdk.debug.installPanicCapture(init.io, setup.paths); + var file_trace_sink: native_sdk.debug.FileTraceSink = undefined; + var fanout_sinks: [2]native_sdk.trace.Sink = undefined; + var fanout_sink: native_sdk.debug.FanoutTraceSink = undefined; + var runtime_trace_sink = trace_sink.sink(); + if (log_setup) |setup| { + file_trace_sink = native_sdk.debug.FileTraceSink.init(init.io, setup.paths.log_dir, setup.paths.log_file, setup.format); + fanout_sinks = .{ trace_sink.sink(), file_trace_sink.sink() }; + fanout_sink = .{ .sinks = &fanout_sinks }; + runtime_trace_sink = fanout_sink.sink(); + } + var shortcut_storage: ShortcutStorage = .{}; + const shortcuts = options.resolvedShortcuts(&shortcut_storage); + // The Runtime is tens of megabytes; construct on the heap (default + // main-thread stacks overflow on a stack instance). + const runtime = try std.heap.page_allocator.create(native_sdk.Runtime); + defer std.heap.page_allocator.destroy(runtime); + native_sdk.Runtime.initAt(runtime, .{ + .platform = null_platform.platform(), + .trace_sink = runtime_trace_sink, + .log_path = if (log_setup) |setup| setup.paths.log_file else null, + .bridge = options.bridge, + .builtin_bridge = options.builtin_bridge, + .security = options.security, + .shortcuts = shortcuts, + .automation = if (build_options.automation) native_sdk.automation.Server.init(init.io, ".zig-cache/native-sdk-automation", app_info.resolvedWindowTitle()) else null, + .window_state_store = store, + }); + + try runtime.run(app); +} + +fn runMacos(app: native_sdk.App, options: RunOptions, init: std.process.Init) !void { + var buffers: StateBuffers = undefined; + var app_info = options.appInfo(&buffers); + const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers); + var mac_platform = try native_sdk.platform.macos.MacPlatform.initWithOptions(native_sdk.geometry.SizeF.init(720, 480), webEngine(), app_info); + defer mac_platform.deinit(); + var trace_sink = StdoutTraceSink{}; + var log_buffers: native_sdk.debug.LogPathBuffers = .{}; + const log_setup = native_sdk.debug.setupLogging(init.io, init.environ_map, app_info.bundle_id, &log_buffers) catch null; + if (log_setup) |setup| native_sdk.debug.installPanicCapture(init.io, setup.paths); + var file_trace_sink: native_sdk.debug.FileTraceSink = undefined; + var fanout_sinks: [2]native_sdk.trace.Sink = undefined; + var fanout_sink: native_sdk.debug.FanoutTraceSink = undefined; + var runtime_trace_sink = trace_sink.sink(); + if (log_setup) |setup| { + file_trace_sink = native_sdk.debug.FileTraceSink.init(init.io, setup.paths.log_dir, setup.paths.log_file, setup.format); + fanout_sinks = .{ trace_sink.sink(), file_trace_sink.sink() }; + fanout_sink = .{ .sinks = &fanout_sinks }; + runtime_trace_sink = fanout_sink.sink(); + } + var shortcut_storage: ShortcutStorage = .{}; + const shortcuts = options.resolvedShortcuts(&shortcut_storage); + // The Runtime is tens of megabytes; construct on the heap (default + // main-thread stacks overflow on a stack instance). + const runtime = try std.heap.page_allocator.create(native_sdk.Runtime); + defer std.heap.page_allocator.destroy(runtime); + native_sdk.Runtime.initAt(runtime, .{ + .platform = mac_platform.platform(), + .trace_sink = runtime_trace_sink, + .log_path = if (log_setup) |setup| setup.paths.log_file else null, + .bridge = options.bridge, + .builtin_bridge = options.builtin_bridge, + .security = options.security, + .shortcuts = shortcuts, + .automation = if (build_options.automation) native_sdk.automation.Server.init(init.io, ".zig-cache/native-sdk-automation", app_info.resolvedWindowTitle()) else null, + .window_state_store = store, + }); + + try runtime.run(app); +} + +fn runLinux(app: native_sdk.App, options: RunOptions, init: std.process.Init) !void { + var buffers: StateBuffers = undefined; + var app_info = options.appInfo(&buffers); + const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers); + var linux_platform = try native_sdk.platform.linux.LinuxPlatform.initWithOptions(native_sdk.geometry.SizeF.init(720, 480), webEngine(), app_info); + defer linux_platform.deinit(); + var trace_sink = StdoutTraceSink{}; + var log_buffers: native_sdk.debug.LogPathBuffers = .{}; + const log_setup = native_sdk.debug.setupLogging(init.io, init.environ_map, app_info.bundle_id, &log_buffers) catch null; + if (log_setup) |setup| native_sdk.debug.installPanicCapture(init.io, setup.paths); + var file_trace_sink: native_sdk.debug.FileTraceSink = undefined; + var fanout_sinks: [2]native_sdk.trace.Sink = undefined; + var fanout_sink: native_sdk.debug.FanoutTraceSink = undefined; + var runtime_trace_sink = trace_sink.sink(); + if (log_setup) |setup| { + file_trace_sink = native_sdk.debug.FileTraceSink.init(init.io, setup.paths.log_dir, setup.paths.log_file, setup.format); + fanout_sinks = .{ trace_sink.sink(), file_trace_sink.sink() }; + fanout_sink = .{ .sinks = &fanout_sinks }; + runtime_trace_sink = fanout_sink.sink(); + } + var shortcut_storage: ShortcutStorage = .{}; + const shortcuts = options.resolvedShortcuts(&shortcut_storage); + // The Runtime is tens of megabytes; construct on the heap (default + // main-thread stacks overflow on a stack instance). + const runtime = try std.heap.page_allocator.create(native_sdk.Runtime); + defer std.heap.page_allocator.destroy(runtime); + native_sdk.Runtime.initAt(runtime, .{ + .platform = linux_platform.platform(), + .trace_sink = runtime_trace_sink, + .log_path = if (log_setup) |setup| setup.paths.log_file else null, + .bridge = options.bridge, + .builtin_bridge = options.builtin_bridge, + .security = options.security, + .shortcuts = shortcuts, + .automation = if (build_options.automation) native_sdk.automation.Server.init(init.io, ".zig-cache/native-sdk-automation", app_info.resolvedWindowTitle()) else null, + .window_state_store = store, + }); + + try runtime.run(app); +} + +fn runWindows(app: native_sdk.App, options: RunOptions, init: std.process.Init) !void { + var buffers: StateBuffers = undefined; + var app_info = options.appInfo(&buffers); + const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers); + var windows_platform = try native_sdk.platform.windows.WindowsPlatform.initWithOptions(native_sdk.geometry.SizeF.init(720, 480), webEngine(), app_info); + defer windows_platform.deinit(); + var trace_sink = StdoutTraceSink{}; + var log_buffers: native_sdk.debug.LogPathBuffers = .{}; + const log_setup = native_sdk.debug.setupLogging(init.io, init.environ_map, app_info.bundle_id, &log_buffers) catch null; + if (log_setup) |setup| native_sdk.debug.installPanicCapture(init.io, setup.paths); + var file_trace_sink: native_sdk.debug.FileTraceSink = undefined; + var fanout_sinks: [2]native_sdk.trace.Sink = undefined; + var fanout_sink: native_sdk.debug.FanoutTraceSink = undefined; + var runtime_trace_sink = trace_sink.sink(); + if (log_setup) |setup| { + file_trace_sink = native_sdk.debug.FileTraceSink.init(init.io, setup.paths.log_dir, setup.paths.log_file, setup.format); + fanout_sinks = .{ trace_sink.sink(), file_trace_sink.sink() }; + fanout_sink = .{ .sinks = &fanout_sinks }; + runtime_trace_sink = fanout_sink.sink(); + } + var shortcut_storage: ShortcutStorage = .{}; + const shortcuts = options.resolvedShortcuts(&shortcut_storage); + // The Runtime is tens of megabytes; construct on the heap (default + // main-thread stacks overflow on a stack instance). + const runtime = try std.heap.page_allocator.create(native_sdk.Runtime); + defer std.heap.page_allocator.destroy(runtime); + native_sdk.Runtime.initAt(runtime, .{ + .platform = windows_platform.platform(), + .trace_sink = runtime_trace_sink, + .log_path = if (log_setup) |setup| setup.paths.log_file else null, + .bridge = options.bridge, + .builtin_bridge = options.builtin_bridge, + .security = options.security, + .shortcuts = shortcuts, + .automation = if (build_options.automation) native_sdk.automation.Server.init(init.io, ".zig-cache/native-sdk-automation", app_info.resolvedWindowTitle()) else null, + .window_state_store = store, + }); + + try runtime.run(app); +} + +fn shouldTrace(record: native_sdk.trace.Record) bool { + if (comptime std.mem.eql(u8, build_options.trace, "off")) return false; + if (comptime std.mem.eql(u8, build_options.trace, "all")) return true; + if (comptime std.mem.eql(u8, build_options.trace, "events")) return true; + return std.mem.indexOf(u8, record.name, build_options.trace) != null; +} + +fn webEngine() native_sdk.WebEngine { + if (comptime std.mem.eql(u8, build_options.web_engine, "chromium")) return .chromium; + return .system; +} + +const StateBuffers = struct { + state_dir: [1024]u8 = undefined, + file_path: [1200]u8 = undefined, + read: [8192]u8 = undefined, + restored_windows: [native_sdk.platform.max_windows]native_sdk.WindowOptions = undefined, +}; + +fn prepareStateStore(io: std.Io, env_map: *std.process.Environ.Map, app_info: *native_sdk.AppInfo, buffers: *StateBuffers) ?native_sdk.window_state.Store { + const paths = native_sdk.window_state.defaultPaths(&buffers.state_dir, &buffers.file_path, app_info.bundle_id, native_sdk.debug.envFromMap(env_map)) catch return null; + const store = native_sdk.window_state.Store.init(io, paths.state_dir, paths.file_path); + if (app_info.windows.len > 0) { + const restored_windows = buffers.restored_windows[0..app_info.windows.len]; + for (restored_windows, 0..) |*window, index| { + if (!window.restore_state) continue; + if (store.loadWindow(window.label, &buffers.read) catch null) |saved| { + window.default_frame = saved.frame; + if (index == 0) app_info.main_window.default_frame = saved.frame; + } + } + } else if (app_info.main_window.restore_state) { + if (store.loadWindow(app_info.main_window.label, &buffers.read) catch null) |saved| { + app_info.main_window.default_frame = saved.frame; + } + } + return store; +}