4e015e925f
* feat(updater): add native macOS app updates - Add Ed25519-signed feeds, verified downloads, atomic replacement, rollback, and relaunch. - Add manifest support plus key generation, feed signing, and updater ZIP packaging. - Document and test the end-to-end macOS update workflow. * fix(updater): harden macOS update lifecycle * fix(updater): address review findings * fix(updater): close review gaps * fix(updater): bind installed identity and versions
2896 lines
151 KiB
Zig
2896 lines
151 KiB
Zig
//! Framework build helper: `addApp` gives a markup/builder app a complete
|
|
//! build (exe, run, test) from a ~5-line build.zig. The app supplies
|
|
//! src/main.zig, app.zon, and assets; the runner and all framework modules
|
|
//! come from the native-sdk dependency.
|
|
|
|
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
const json_to_zon = @import("../src/tooling/json_to_zon.zig");
|
|
|
|
/// Canonicalize a generated file by its CONTENT before another build step
|
|
/// consumes it. `std.Build.Step.Run` normally places outputs under a cache
|
|
/// directory keyed by every declared input. That is correct for the producer,
|
|
/// but it means an unrelated input can change the output PATH even when the
|
|
/// file's bytes are identical; downstream Run steps hash that path and miss
|
|
/// their own caches. TypeScript's combined frontend is exactly that shape:
|
|
/// service implementation edits re-run the checker while often leaving the
|
|
/// core ABI contract byte-identical.
|
|
///
|
|
/// This narrow adapter gives equal bytes one immutable cache path. Consumers
|
|
/// still invalidate whenever the bytes change, while producer-only churn
|
|
/// stops here. It is public so the repository's fixture graph can exercise
|
|
/// the same boundary as app builds.
|
|
pub fn stabilizeGeneratedFile(b: *std.Build, source: std.Build.LazyPath, basename: []const u8, trace: bool) std.Build.LazyPath {
|
|
return StableGeneratedFile.create(b, source, basename, trace).lazyPath();
|
|
}
|
|
|
|
const StableGeneratedFile = struct {
|
|
step: std.Build.Step,
|
|
source: std.Build.LazyPath,
|
|
basename: []const u8,
|
|
trace: bool,
|
|
generated: std.Build.GeneratedFile,
|
|
|
|
fn create(b: *std.Build, source: std.Build.LazyPath, basename: []const u8, trace: bool) *StableGeneratedFile {
|
|
const stable = b.allocator.create(StableGeneratedFile) catch @panic("OOM");
|
|
stable.* = .{
|
|
.step = std.Build.Step.init(.{
|
|
.id = .custom,
|
|
.name = b.fmt("stabilize generated {s}", .{basename}),
|
|
.owner = b,
|
|
.makeFn = make,
|
|
}),
|
|
.source = source.dupe(b),
|
|
.basename = b.dupePath(basename),
|
|
.trace = trace,
|
|
.generated = undefined,
|
|
};
|
|
stable.generated = .{ .step = &stable.step };
|
|
source.addStepDependencies(&stable.step);
|
|
return stable;
|
|
}
|
|
|
|
fn lazyPath(self: *StableGeneratedFile) std.Build.LazyPath {
|
|
return .{ .generated = .{ .file = &self.generated } };
|
|
}
|
|
|
|
fn make(step: *std.Build.Step, options: std.Build.Step.MakeOptions) !void {
|
|
_ = options;
|
|
const self: *StableGeneratedFile = @fieldParentPtr("step", step);
|
|
const b = step.owner;
|
|
const io = b.graph.io;
|
|
const arena = b.allocator;
|
|
const source_path = self.source.getPath3(b, step);
|
|
const bytes = source_path.root_dir.handle.readFileAlloc(io, source_path.sub_path, arena, .limited(64 * 1024 * 1024)) catch |err|
|
|
return step.fail("cannot stabilize generated {s}: {t}", .{ self.basename, err });
|
|
|
|
var digest: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined;
|
|
std.crypto.hash.sha2.Sha256.hash(bytes, &digest, .{});
|
|
const hex = std.fmt.bytesToHex(digest, .lower);
|
|
const output_dir = b.pathJoin(&.{ "o", "native-stable", &hex });
|
|
const output_path = b.pathJoin(&.{ output_dir, self.basename });
|
|
self.generated.path = try b.cache_root.join(arena, &.{output_path});
|
|
|
|
var reused = false;
|
|
if (b.cache_root.handle.readFileAlloc(io, output_path, arena, .limited(64 * 1024 * 1024))) |existing| {
|
|
reused = std.mem.eql(u8, existing, bytes);
|
|
} else |_| {}
|
|
if (!reused) {
|
|
b.cache_root.handle.createDirPath(io, output_dir) catch |err|
|
|
return step.fail("cannot create the stable generated-output directory for {s}: {t}", .{ self.basename, err });
|
|
var atomic = b.cache_root.handle.createFileAtomic(io, output_path, .{ .replace = true }) catch |err|
|
|
return step.fail("cannot stage stable generated {s}: {t}", .{ self.basename, err });
|
|
defer atomic.deinit(io);
|
|
atomic.file.writeStreamingAll(io, bytes) catch |err|
|
|
return step.fail("cannot write stable generated {s}: {t}", .{ self.basename, err });
|
|
atomic.replace(io) catch |err|
|
|
return step.fail("cannot publish stable generated {s}: {t}", .{ self.basename, err });
|
|
}
|
|
step.result_cached = reused;
|
|
if (self.trace) std.debug.print("native build trace: {s} content {s} {s}\n", .{
|
|
self.basename,
|
|
&hex,
|
|
if (reused) "reused" else "changed",
|
|
});
|
|
}
|
|
};
|
|
|
|
/// The shared web-layer inference contract: this build graph is one thin
|
|
/// adapter over it (the CLI's manifest tooling and the app runner are the
|
|
/// others), feeding it the inputs only this boundary sees — the
|
|
/// `-Dweb-engine` and `-Dweb-layer` flags resolved against app.zon.
|
|
const web_layer_contract = @import("../src/primitives/app_manifest/web_layer.zig");
|
|
|
|
const PlatformOption = enum {
|
|
auto,
|
|
null,
|
|
macos,
|
|
linux,
|
|
windows,
|
|
};
|
|
|
|
const TraceOption = enum {
|
|
off,
|
|
events,
|
|
runtime,
|
|
all,
|
|
};
|
|
|
|
const WebEngineOption = web_layer_contract.WebEngine;
|
|
|
|
const WebLayerOption = web_layer_contract.WebViewLayer;
|
|
|
|
pub const AppOptions = struct {
|
|
name: []const u8,
|
|
/// Explicit manifest path. Null auto-detects app.json first, then app.zon,
|
|
/// so older owned build.zig files can adopt JSON without a build edit.
|
|
manifest: ?[]const u8 = null,
|
|
/// App entry point; defaults to src/main.zig (relative to `app_root`).
|
|
main: []const u8 = "src/main.zig",
|
|
/// Root of the app source tree, relative to the build root. "." for a
|
|
/// build.zig that lives in the app directory (every ejected app). The
|
|
/// CLI's generated build graph under `<app>/.native/build/` passes
|
|
/// "../.." so `src/`, `app.zon`, and `assets/` keep resolving in the
|
|
/// app directory rather than the cache directory.
|
|
app_root: []const u8 = ".",
|
|
/// Live `<terminal>` sessions: set true AND pin ghostty as a LAZY
|
|
/// dependency in the app's OWN build.zig.zon (the examples/workbench
|
|
/// pin) — the framework then resolves `ghostty-vt` with the app
|
|
/// module's target/optimize and the safe flags (simd off, no macOS
|
|
/// app or xcframework artifacts), and the runtime owns the emulator
|
|
/// behind every `<terminal pty={key}>` binding. Default false wires
|
|
/// the stub: `<terminal>` renders the honest empty surface, and the
|
|
/// build never traverses ghostty's dependency graph (whose configure
|
|
/// step walks wuffs/translate_c and whose full build pulls harfbuzz)
|
|
/// — the load-bearing property for scaffolded apps.
|
|
terminal_sessions: bool = false,
|
|
};
|
|
|
|
/// Which core the app tree carries. No flag and no config anywhere: the
|
|
/// tree IS the truth — `src/core.ts` is a TypeScript core (compiled to
|
|
/// native code at build time, run through generated wiring),
|
|
/// `src/main.zig` a Zig one, and both at once is a teaching error
|
|
/// naming the two files.
|
|
const CoreTree = enum { zig, ts, both, neither };
|
|
|
|
fn detectCoreTree(b: *std.Build, app_root: []const u8) CoreTree {
|
|
const has_ts = appFileExists(b, app_root, "src/core.ts");
|
|
const has_zig = appFileExists(b, app_root, "src/main.zig");
|
|
if (has_ts and has_zig) return .both;
|
|
if (has_ts) return .ts;
|
|
if (has_zig) return .zig;
|
|
return .neither;
|
|
}
|
|
|
|
fn appFileExists(b: *std.Build, app_root: []const u8, sub_path: []const u8) bool {
|
|
b.build_root.handle.access(b.graph.io, appPath(b, app_root, sub_path), .{}) catch return false;
|
|
return true;
|
|
}
|
|
|
|
fn appManifestName(b: *std.Build, app_root: []const u8, requested: ?[]const u8) []const u8 {
|
|
if (requested) |name| return name;
|
|
if (appFileExists(b, app_root, "app.json")) return "app.json";
|
|
return "app.zon";
|
|
}
|
|
|
|
fn appManifestPath(b: *std.Build, app_root: []const u8, manifest_name: []const u8) []const u8 {
|
|
return appPath(b, app_root, manifest_name);
|
|
}
|
|
|
|
/// Produce the Zig module consumed by the existing comptime manifest wiring.
|
|
/// ZON manifests are already modules; JSON manifests are converted losslessly
|
|
/// into a generated module, keeping one runtime feature path for both formats.
|
|
fn appManifestModule(b: *std.Build, app_root: []const u8, manifest_name: []const u8) *std.Build.Module {
|
|
const path = appManifestPath(b, app_root, manifest_name);
|
|
if (!json_to_zon.isJsonPath(path)) {
|
|
return b.createModule(.{ .root_source_file = b.path(path) });
|
|
}
|
|
const source = b.build_root.handle.readFileAlloc(b.graph.io, path, b.allocator, .limited(1024 * 1024)) catch
|
|
@panic("cannot read app.json");
|
|
const zon = json_to_zon.convertAlloc(b.allocator, source) catch |err| switch (err) {
|
|
error.NullNotAllowed => @panic("app.json cannot contain null values; omit optional fields instead"),
|
|
else => @panic("cannot convert app.json into the build-time manifest module; run `native check` for a precise diagnostic"),
|
|
};
|
|
const generated = b.addWriteFiles().add("app_manifest.zon", zon);
|
|
return b.createModule(.{ .root_source_file = generated });
|
|
}
|
|
|
|
const TsWindowView = struct {
|
|
label: []const u8,
|
|
source_path: []const u8,
|
|
staged_path: []const u8,
|
|
};
|
|
|
|
const TsWindowSource = struct {
|
|
set_path: []const u8,
|
|
source_path: []const u8,
|
|
staged_path: []const u8,
|
|
};
|
|
|
|
const TsWindowViews = struct {
|
|
views: []const TsWindowView,
|
|
sources: []const TsWindowSource,
|
|
};
|
|
|
|
/// Default TypeScript secondary-window views are statically discovered under
|
|
/// `src/windows/`: `settings.native` serves descriptor label `settings`.
|
|
/// Direct files form the generated launcher's closed, comptime-compiled view
|
|
/// set while nested `.native` files are the shared import source set and
|
|
/// `windows(model)` owns dynamic liveness.
|
|
fn collectTsWindowViews(b: *std.Build, app_root: []const u8) TsWindowViews {
|
|
const windows_path = appPath(b, app_root, "src/windows");
|
|
var dir = b.build_root.handle.openDir(b.graph.io, windows_path, .{ .iterate = true }) catch return .{ .views = &.{}, .sources = &.{} };
|
|
defer dir.close(b.graph.io);
|
|
var walker = dir.walk(b.allocator) catch return .{ .views = &.{}, .sources = &.{} };
|
|
defer walker.deinit();
|
|
var views: std.ArrayList(TsWindowView) = .empty;
|
|
var sources: std.ArrayList(TsWindowSource) = .empty;
|
|
while (walker.next(b.graph.io) catch null) |entry| {
|
|
if (entry.kind != .file or !std.mem.endsWith(u8, entry.path, ".native")) continue;
|
|
const normalized_path = b.dupe(entry.path);
|
|
for (normalized_path) |*char| {
|
|
if (char.* == '\\') char.* = '/';
|
|
}
|
|
sources.append(b.allocator, .{
|
|
.set_path = normalized_path,
|
|
.source_path = b.fmt("src/windows/{s}", .{normalized_path}),
|
|
.staged_path = b.fmt("windows/{s}", .{normalized_path}),
|
|
}) catch @panic("OOM");
|
|
// Direct children are label-addressed window roots. Nested files
|
|
// are component sources those roots may import transitively.
|
|
if (std.mem.indexOfScalar(u8, entry.path, '/') != null or std.mem.indexOfScalar(u8, entry.path, '\\') != null) continue;
|
|
const label = entry.path[0 .. entry.path.len - ".native".len];
|
|
if (label.len == 0 or label.len > 64 or std.mem.eql(u8, label, ".") or std.mem.eql(u8, label, "..")) {
|
|
@panic("\ninvalid TypeScript secondary-window view filename: use src/windows/<label>.native with a non-empty label of at most 64 bytes\n");
|
|
}
|
|
for (label) |ch| {
|
|
if (ch == 0 or ch == '/' or ch == '\\') {
|
|
@panic("\ninvalid TypeScript secondary-window view filename: the filename stem must be a valid window label\n");
|
|
}
|
|
}
|
|
views.append(b.allocator, .{
|
|
.label = b.dupe(label),
|
|
.source_path = b.fmt("src/windows/{s}", .{entry.path}),
|
|
.staged_path = b.fmt("windows/{s}", .{entry.path}),
|
|
}) catch @panic("OOM");
|
|
}
|
|
const less = struct {
|
|
fn than(_: void, a: TsWindowView, z: TsWindowView) bool {
|
|
return std.mem.order(u8, a.label, z.label) == .lt;
|
|
}
|
|
}.than;
|
|
std.mem.sort(TsWindowView, views.items, {}, less);
|
|
const source_less = struct {
|
|
fn than(_: void, a: TsWindowSource, z: TsWindowSource) bool {
|
|
return std.mem.order(u8, a.set_path, z.set_path) == .lt;
|
|
}
|
|
}.than;
|
|
std.mem.sort(TsWindowSource, sources.items, {}, source_less);
|
|
return .{ .views = views.items, .sources = sources.items };
|
|
}
|
|
|
|
fn tsWindowRegistrySource(b: *std.Build, registry: TsWindowViews) []const u8 {
|
|
const views = registry.views;
|
|
var out: std.ArrayList(u8) = .empty;
|
|
out.appendSlice(b.allocator,
|
|
\\//! Generated by build/app.zig from src/windows/<label>.native.
|
|
\\const std = @import("std");
|
|
\\const native_sdk = @import("native_sdk");
|
|
\\const core = @import("core.zig");
|
|
\\const canvas = native_sdk.canvas;
|
|
\\const Adapter = native_sdk.TsUiApp(core);
|
|
\\const App = Adapter.App;
|
|
\\
|
|
) catch @panic("OOM");
|
|
if (registry.sources.len > 0) {
|
|
out.appendSlice(b.allocator,
|
|
\\const window_sources = [_]canvas.ui_markup.SourceFile{
|
|
\\
|
|
) catch @panic("OOM");
|
|
for (registry.sources) |source| {
|
|
const line = std.fmt.allocPrint(
|
|
b.allocator,
|
|
" .{{ .path = \"{f}\", .source = @embedFile(\"{f}\") }},\n",
|
|
.{ std.zig.fmtString(source.set_path), std.zig.fmtString(source.staged_path) },
|
|
) catch @panic("OOM");
|
|
out.appendSlice(b.allocator, line) catch @panic("OOM");
|
|
}
|
|
out.appendSlice(b.allocator, "};\n") catch @panic("OOM");
|
|
}
|
|
for (views, 0..) |view, index| {
|
|
const line = std.fmt.allocPrint(
|
|
b.allocator,
|
|
"const WindowView{d} = canvas.CompiledMarkupImports(core.Model, core.Msg, \"{f}\", &window_sources);\n",
|
|
.{ index, std.zig.fmtString(std.fs.path.basename(view.staged_path)) },
|
|
) catch @panic("OOM");
|
|
out.appendSlice(b.allocator, line) catch @panic("OOM");
|
|
}
|
|
if (views.len == 0) {
|
|
out.appendSlice(b.allocator,
|
|
\\pub fn build(_: *App.Ui, _: *const core.Model, _: []const u8) App.Ui.Node {
|
|
\\ @compileError("this TypeScript core exports windows(model), but the app has no src/windows/<label>.native views");
|
|
\\}
|
|
\\pub const fragments = [_]canvas.MarkupFragment{};
|
|
\\
|
|
) catch @panic("OOM");
|
|
return out.items;
|
|
}
|
|
out.appendSlice(b.allocator,
|
|
\\pub fn build(ui: *App.Ui, model: *const core.Model, label: []const u8) App.Ui.Node {
|
|
\\
|
|
) catch @panic("OOM");
|
|
for (views, 0..) |view, index| {
|
|
const line = std.fmt.allocPrint(
|
|
b.allocator,
|
|
" if (std.mem.eql(u8, label, \"{f}\")) return WindowView{d}.build(ui, model);\n",
|
|
.{ std.zig.fmtString(view.label), index },
|
|
) catch @panic("OOM");
|
|
out.appendSlice(b.allocator, line) catch @panic("OOM");
|
|
}
|
|
out.appendSlice(b.allocator,
|
|
\\ @panic("windows(model) declared a label with no src/windows/<label>.native view");
|
|
\\}
|
|
\\pub const fragments = [_]canvas.MarkupFragment{
|
|
\\
|
|
) catch @panic("OOM");
|
|
for (views, 0..) |view, index| {
|
|
const line = std.fmt.allocPrint(
|
|
b.allocator,
|
|
" WindowView{d}.fragment(\"{f}\"),\n",
|
|
.{ index, std.zig.fmtString(view.source_path) },
|
|
) catch @panic("OOM");
|
|
out.appendSlice(b.allocator, line) catch @panic("OOM");
|
|
}
|
|
out.appendSlice(b.allocator, "};\n") catch @panic("OOM");
|
|
return out.items;
|
|
}
|
|
|
|
/// How a TypeScript core compiles: through the external core compiler,
|
|
/// always. The frontend checks the core and emits its contract sidecar,
|
|
/// corewire projects the compile entry and profile, the exact-pinned
|
|
/// external toolchain builds the archive, and the app links corewire's
|
|
/// generated mirror over it. `.core_compiler` in app.zon (and the
|
|
/// `-Dcore-compiler` flag) accept only "external" — the value exists so
|
|
/// a stated choice stays stateable; the removed transpiled lane's
|
|
/// spelling is refused with a teaching (see resolveCoreCompiler).
|
|
const core_compiler_teaching =
|
|
"\ncore_compiler = \"transpiler\" names the removed TS-to-Zig transpiled lane (v0.7.0" ++
|
|
" removed it): a TypeScript core compiles through the external core compiler now, and" ++
|
|
" that is the default.\nDelete the setting (or spell it \"external\").\n";
|
|
|
|
/// The staged TypeScript-core wiring: one generated directory holding the
|
|
/// core module (the generated mirror with its staged shim runtime) and the
|
|
/// SDK's generated-wiring entry (ts_core_main.zig as main.zig) — plus the
|
|
/// compiled-core archive and isolated markup data object the app module
|
|
/// links. Built once per app build and shared by the exe and test modules.
|
|
const TsCoreStage = struct {
|
|
main_root: std.Build.LazyPath,
|
|
/// The staged mobile wiring (ts_core_mobile.zig beside the same
|
|
/// mirror/registry files): the embed static library's `app`
|
|
/// module roots here on iOS/Android targets.
|
|
mobile_root: std.Build.LazyPath,
|
|
/// The compiled-core archive: the app module links it (with libc,
|
|
/// for the toolchain's runtime) beside the staged mirror.
|
|
archive: std.Build.LazyPath,
|
|
/// Plain-scriptc service host compiled from src/services/, when that
|
|
/// class exists and the child carrier is selected. It is installed/
|
|
/// packaged beside the app executable.
|
|
service_exe: ?std.Build.LazyPath = null,
|
|
/// The in-process carrier's service archive (thread-instanced,
|
|
/// runtime-localized), when that class exists and the in-process
|
|
/// carrier is selected. The app module links it beside the core's
|
|
/// archive.
|
|
service_archive: ?std.Build.LazyPath = null,
|
|
/// C source containing the primary markup bytes under stable external
|
|
/// symbols. Compiled as a tiny data object by the final app artifact.
|
|
markup_c: std.Build.LazyPath,
|
|
migrations: std.Build.LazyPath,
|
|
};
|
|
|
|
/// Which carrier runs src/services/ operations. Auto preserves the isolated,
|
|
/// single-instance child carrier; apps can explicitly opt into the parallel
|
|
/// in-process pool wherever the pinned service compiler can build the
|
|
/// runtime-localized service archive for the target (see
|
|
/// serviceArchiveSupported). `.service_carrier` in app.zon and the
|
|
/// `-Dservice-carrier` flag state the choice.
|
|
const ServiceCarrierOption = enum { auto, in_process, child };
|
|
|
|
const ServiceCarrier = enum { none, child, in_process };
|
|
|
|
/// Whether the pinned compiler can build TypeScript archives/executables for
|
|
/// this host/target pairing. Native desktop targets use their host toolchain,
|
|
/// including native Windows/MSVC. Cross-Windows builds use Zig's bundled GNU
|
|
/// sysroot; an MSVC cross target has no CRT headers or libraries to compile
|
|
/// the ScriptC runtime against. Mobile targets are aarch64 only and
|
|
/// archive-only (library mode — the app links the archive; no standalone
|
|
/// executable exists there): iOS device/simulator archives build on a macOS
|
|
/// host against the selected Apple SDK, Android archives build on any
|
|
/// desktop host against a discovered NDK sysroot.
|
|
pub fn scriptcCompileSupported(host: std.Target, target: std.Build.ResolvedTarget) bool {
|
|
const desktop_host = switch (host.os.tag) {
|
|
.macos, .linux, .windows => true,
|
|
else => false,
|
|
};
|
|
if (!desktop_host) return false;
|
|
const cross = scriptcTargetIsCross(host, target);
|
|
return switch (target.result.os.tag) {
|
|
.linux => if (target.result.abi.isAndroid()) target.result.cpu.arch == .aarch64 else true,
|
|
.windows => !cross or target.result.abi == .gnu,
|
|
.macos => host.os.tag == .macos,
|
|
.ios => host.os.tag == .macos and target.result.cpu.arch == .aarch64,
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
/// Whether the pinned service compiler can build the in-process carrier's
|
|
/// archive (runtime-localized, thread-instanced) on this build host for
|
|
/// this target. Its object localizers are deliberately architecture-aware:
|
|
/// native Linux uses host binutils, cross-ELF accepts x86_64/aarch64
|
|
/// (Android's aarch64 archives ride this lane), COFF accepts x86_64 (GNU
|
|
/// when cross-compiled; the host ABI when native), and Mach-O — macOS, iOS
|
|
/// device, and iOS simulator — needs a macOS host (Apple linking rides the
|
|
/// host toolchain's SDK).
|
|
pub fn serviceArchiveSupported(host: std.Target, target: std.Build.ResolvedTarget) bool {
|
|
if (!scriptcCompileSupported(host, target)) return false;
|
|
const cross = scriptcTargetIsCross(host, target);
|
|
return switch (target.result.os.tag) {
|
|
.linux => !cross or target.result.cpu.arch == .x86_64 or target.result.cpu.arch == .aarch64,
|
|
.windows => target.result.cpu.arch == .x86_64,
|
|
.macos, .ios => host.os.tag == .macos,
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
/// Whether a Linux target's spelling lands on Zig's default glibc floor,
|
|
/// which predates `arc4random_buf` — a symbol the compiled service
|
|
/// runtime references, so the link fails late and opaquely without the
|
|
/// configure-time teaching. A native target (no `-Dtarget` os) resolves
|
|
/// the system's own glibc and is exempt; an explicitly spelled `-gnu`
|
|
/// target needs a stated glibc of 2.36 or later (or `-musl`, whose libc
|
|
/// always has the symbol).
|
|
pub fn linuxGlibcSpellingHitsDefaultFloor(target: std.Build.ResolvedTarget) bool {
|
|
if (target.result.os.tag != .linux or !target.result.abi.isGnu()) return false;
|
|
if (target.query.os_tag == null) return false;
|
|
const glibc = target.query.glibc_version orelse return true;
|
|
return glibc.order(.{ .major = 2, .minor = 36, .patch = 0 }) == .lt;
|
|
}
|
|
|
|
/// Whether ScriptC needs its zig-cc target lane rather than the native host
|
|
/// compiler. Keep this identical to the driver scripts' platform-triple
|
|
/// comparison: a stated glibc version makes an otherwise same-triple Linux
|
|
/// target cross because that floor must reach Zig's `-target` argument.
|
|
pub fn scriptcTargetIsCross(host: std.Target, target: std.Build.ResolvedTarget) bool {
|
|
if (target.result.os.tag != host.os.tag or target.result.cpu.arch != host.cpu.arch) return true;
|
|
// Zig's Windows host triple defaults to GNU even when native clang uses
|
|
// the installed Windows toolchain. A same-architecture MSVC target is
|
|
// therefore still native compiler work. Preserve an exact native GNU
|
|
// triple too; every other ABI change needs the zig-cc cross lane.
|
|
if (target.result.os.tag == .windows) {
|
|
return target.result.abi != host.abi and target.result.abi != .msvc;
|
|
}
|
|
return target.result.abi != host.abi or target.query.glibc_version != null;
|
|
}
|
|
|
|
/// The glibc-floor teaching for explicitly targeted ScriptC builds. Both the
|
|
/// core archive and either service carrier carry the compiled TypeScript
|
|
/// runtime, so all of them hit the same missing symbol. This is independent of
|
|
/// whether the target triple happens to match the build host.
|
|
pub fn scriptcLinuxGlibcSpellingTeaching(b: *std.Build, target: std.Build.ResolvedTarget) []const u8 {
|
|
return b.fmt(
|
|
"\nTypeScript target {t}-linux-gnu uses Zig's default glibc floor, which" ++
|
|
" predates arc4random_buf — a symbol the compiled runtime needs (glibc" ++
|
|
" 2.36+).\nSpell the target as \"{t}-linux-gnu.2.36\" (or later), or" ++
|
|
" \"{t}-linux-musl\".\n",
|
|
.{ target.result.cpu.arch, target.result.cpu.arch, target.result.cpu.arch },
|
|
);
|
|
}
|
|
|
|
pub fn panicScriptcLinuxGlibcSpelling(b: *std.Build, target: std.Build.ResolvedTarget) noreturn {
|
|
@panic(scriptcLinuxGlibcSpellingTeaching(b, target));
|
|
}
|
|
|
|
pub fn panicUnsupportedScriptcTarget(b: *std.Build, host: std.Target, target: std.Build.ResolvedTarget) noreturn {
|
|
if (target.result.os.tag == .windows and scriptcTargetIsCross(host, target) and target.result.abi != .gnu) {
|
|
@panic(b.fmt(
|
|
"\nTypeScript cross-target Windows builds require the GNU ABI: Zig supplies" ++
|
|
" that target's CRT and system libraries, while an MSVC target needs a native" ++
|
|
" Windows toolchain.\nBuild {t}-windows-msvc on a matching Windows host, or" ++
|
|
" cross-compile as \"{t}-windows-gnu\".\n",
|
|
.{ target.result.cpu.arch, target.result.cpu.arch },
|
|
));
|
|
}
|
|
if (target.result.os.tag == .ios and host.os.tag != .macos) {
|
|
@panic(
|
|
"\nTypeScript iOS builds run on a macOS build host only: the Apple SDK sysroot" ++
|
|
" and Mach-O symbol localization live there.\nBuild iOS apps on a Mac.\n",
|
|
);
|
|
}
|
|
@panic(
|
|
"\nTypeScript builds support native host targets, Linux and Windows GNU cross" ++
|
|
" targets from macOS/Linux/Windows, macOS targets from macOS, and the mobile" ++
|
|
" targets aarch64 iOS/iOS-simulator (from macOS) and aarch64 Android." ++
|
|
"\nChoose a supported target/host pairing.\n",
|
|
);
|
|
}
|
|
|
|
fn resolveServiceCarrier(
|
|
choice: ServiceCarrierOption,
|
|
has_services: bool,
|
|
b: *std.Build,
|
|
target: std.Build.ResolvedTarget,
|
|
) ServiceCarrier {
|
|
if (!has_services) return .none;
|
|
const host = b.graph.host.result;
|
|
const supported = serviceArchiveSupported(host, target);
|
|
// Mobile has no child processes, so the in-process pool is the only
|
|
// carrier there: auto resolves to it, and an explicit "child" is a
|
|
// stated impossibility, taught rather than quietly rewritten.
|
|
if (target.result.os.tag == .ios or target.result.abi.isAndroid()) {
|
|
return switch (choice) {
|
|
.auto, .in_process => if (supported) .in_process else panicUnsupportedScriptcTarget(b, host, target),
|
|
.child => @panic(
|
|
"\nservice_carrier = \"child\" is unavailable on mobile targets: iOS and Android" ++
|
|
" apps cannot spawn a sibling service process, so src/services operations run" ++
|
|
" on the in-process pool there.\nUse \"auto\" or \"in_process\" (desktop" ++
|
|
" builds of the same app keep the child carrier under auto).\n",
|
|
),
|
|
};
|
|
}
|
|
const carrier: ServiceCarrier = switch (choice) {
|
|
.auto, .child => .child,
|
|
.in_process => if (supported) .in_process else @panic(
|
|
"\nservice_carrier = \"in_process\" requires a target the pinned service compiler" ++
|
|
" can produce a runtime-localized archive for: native Linux, cross-Linux" ++
|
|
" x86_64/aarch64, native Windows x86_64, cross-Windows x86_64 GNU," ++
|
|
" macOS from a macOS build host, or a mobile target." ++
|
|
"\nUse \"child\" (or drop the setting — auto selects the child carrier)" ++
|
|
" for this target.\n",
|
|
),
|
|
};
|
|
return carrier;
|
|
}
|
|
|
|
/// The `arch-os-abi` platform spelling every ScriptC compile lane receives.
|
|
/// A stated glibc version rides the abi (`gnu.2.36`) so the compiler's
|
|
/// cross lane builds at the spelled floor rather than the default one.
|
|
pub fn scriptcPlatformTriple(b: *std.Build, target: std.Build.ResolvedTarget) []const u8 {
|
|
const triple = b.fmt("{t}-{t}-{t}", .{ target.result.cpu.arch, target.result.os.tag, target.result.abi });
|
|
if (target.result.os.tag != .linux or !target.result.abi.isGnu()) return triple;
|
|
const glibc = target.query.glibc_version orelse return triple;
|
|
return if (glibc.patch == 0)
|
|
b.fmt("{s}.{d}.{d}", .{ triple, glibc.major, glibc.minor })
|
|
else
|
|
b.fmt("{s}.{d}.{d}.{d}", .{ triple, glibc.major, glibc.minor, glibc.patch });
|
|
}
|
|
|
|
fn parseServiceCarrierOption(raw: []const u8) ServiceCarrierOption {
|
|
if (std.mem.eql(u8, raw, "auto")) return .auto;
|
|
if (std.mem.eql(u8, raw, "in_process")) return .in_process;
|
|
if (std.mem.eql(u8, raw, "child")) return .child;
|
|
@panic("\n.service_carrier must be \"auto\", \"in_process\", or \"child\"\n");
|
|
}
|
|
|
|
/// The frontend's own sources — the staleness set of every build step
|
|
/// that runs it (a frontend edit re-checks every core).
|
|
const frontend_sources = [_][]const u8{
|
|
"checker.ts", "cli.ts", "contract.ts", "diagnostics.ts", "frontend.ts", "infer.ts", "modules.ts", "ownership.ts", "service_contract.ts", "sqlite_codegen.ts", "sqlite_cli.ts", "sqlite_runtime_policy.ts", "typed_ast.ts", "types.ts", "wyhash.ts",
|
|
};
|
|
|
|
/// Whether the frontend's TypeScript compiler (@typescript/old, the
|
|
/// exactly pinned npm alias of the real `typescript` package) RESOLVES
|
|
/// from the SDK's packages/core, by node's ancestor node_modules walk —
|
|
/// at the SDK's exactly pinned VERSION. The same semantics the CLI gates
|
|
/// on (src/tooling/ts_core.zig
|
|
/// transpilerResolution, this predicate's deliberate twin: keep the two
|
|
/// in lockstep).
|
|
///
|
|
/// Validation tracks ONLY what runtime loads, from the same origin
|
|
/// runtime resolves from: typed_ast.ts imports "@typescript/old" directly
|
|
/// and build/ts_run.mjs's load hook requires it from the target
|
|
/// packages/core/src module — src/ never carries a node_modules, so
|
|
/// packages/core is the walk origin that mirrors both. Covers every
|
|
/// layout: a repo checkout's packages/core/node_modules (nearest wins),
|
|
/// and the npm-installed CLI whose own `dependencies` carry the alias
|
|
/// (nested under the package on global prefixes, hoisted to the project
|
|
/// root on local ones, pnpm's sibling node_modules).
|
|
///
|
|
/// A stray `@typescript/typescript6` compat wrapper in a consumer tree
|
|
/// (a former dependency of this package, or the consumer's own) is
|
|
/// deliberately NOT probed: nothing imports it at run time, and holding
|
|
/// the alias's version as seen FROM a wrapper's origin against the pin
|
|
/// can only FALSE-REJECT healthy trees — a consumer's hoisted conflicting
|
|
/// `@typescript/old` wins the walk from there while the copy runtime
|
|
/// actually loads sits correctly pinned under packages/core.
|
|
///
|
|
/// Resolvable means the alias's manifest AND its entrypoint are present
|
|
/// (see tsAliasedCompilerVersion for node's error shape and the
|
|
/// mid-extraction slivers) and its installed version equals the pin —
|
|
/// read from the SDK dependency's own packages/core/package.json (the
|
|
/// `npm:typescript@X.Y.Z` alias suffix in devDependencies —
|
|
/// tsParseAliasedCompilerPin, mirroring the twin's
|
|
/// parseAliasedCompilerPin), never hardcoded, so version bumps stay a
|
|
/// one-file change.
|
|
const TsToolchainResolution = union(enum) {
|
|
resolved,
|
|
unresolved,
|
|
/// The aliased compiler resolves, but at a version other than the
|
|
/// SDK's exact pin (strings allocated on b.allocator, freed never —
|
|
/// this is configure-time teaching data).
|
|
version_mismatch: struct { resolved: []const u8, pinned: []const u8 },
|
|
};
|
|
|
|
fn tsToolchainResolution(b: *std.Build, dep: *std.Build.Dependency) TsToolchainResolution {
|
|
const sdk_root = tsSdkRoot(b.allocator, b.graph.io, dep);
|
|
// The pin first: an SDK tree whose packages/core/package.json is
|
|
// missing or pinless is not one this gate can vouch for (the file
|
|
// ships in every layout), so it concludes unresolved and the teaching
|
|
// path acts.
|
|
const pinned = tsPinnedCompilerVersion(b, sdk_root) orelse return .unresolved;
|
|
const resolved = tsAliasedCompilerVersion(b, b.pathJoin(&.{ sdk_root, "packages", "core" })) orelse return .unresolved;
|
|
if (std.mem.eql(u8, resolved, pinned)) return .resolved;
|
|
return .{ .version_mismatch = .{ .resolved = resolved, .pinned = pinned } };
|
|
}
|
|
|
|
/// How runtime's `import "@typescript/old"` resolves, by node's walk FROM
|
|
/// `origin_dir` upward: the nearest ancestor
|
|
/// `node_modules/@typescript/old` wins (skipping ancestors that are
|
|
/// themselves a node_modules directory, as node does). Callers pass the
|
|
/// SDK's packages/core — the origin typed_ast.ts and ts_run.mjs resolve
|
|
/// the alias from. The walk must be a real ancestor walk, not a
|
|
/// fixed-sibling probe: npm hoists the alias to the install root on flat
|
|
/// layouts and nests it under the CLI on version conflicts. Resolvable
|
|
/// means the alias's manifest AND its entrypoint — the alias is the real
|
|
/// `typescript` package, whose `"main"` is ./lib/typescript.js (no
|
|
/// `"exports"`): a bare directory (an interrupted install, a pruned
|
|
/// node_modules) is MODULE_NOT_FOUND at run time, and a manifest without
|
|
/// its entrypoint (npm extraction is not atomic; package.json rides
|
|
/// first in the tarball) fails just as opaquely. Hardcoding it is safe
|
|
/// because the alias is exactly pinned (`npm:typescript@X.Y.Z` in both
|
|
/// manifests plus the lockfile) and drift-checked by check-version-sync.
|
|
/// A manifest without its entrypoint THROWS in node rather than
|
|
/// consulting a deeper ancestor, so it concludes unresolvable here too
|
|
/// (this predicate's deliberate twin: src/tooling/ts_core.zig's
|
|
/// aliasedCompilerVersion — keep the two in lockstep).
|
|
///
|
|
/// Returns the resolved alias's installed VERSION so the caller can hold
|
|
/// it against the SDK's pin; a resolvable-looking alias whose manifest
|
|
/// carries no version field returns null (every real npm install writes
|
|
/// one — its absence is a corrupt sliver, not a compiler to vouch for).
|
|
fn tsAliasedCompilerVersion(b: *std.Build, origin_dir: []const u8) ?[]const u8 {
|
|
const io = b.graph.io;
|
|
var dir: []const u8 = origin_dir;
|
|
while (true) {
|
|
if (!std.mem.eql(u8, std.fs.path.basename(dir), "node_modules")) {
|
|
found: {
|
|
const manifest_path = b.pathJoin(&.{ dir, "node_modules", "@typescript", "old", "package.json" });
|
|
std.Io.Dir.cwd().access(io, manifest_path, .{}) catch break :found;
|
|
std.Io.Dir.cwd().access(io, b.pathJoin(&.{ dir, "node_modules", "@typescript", "old", "lib", "typescript.js" }), .{}) catch return null;
|
|
const manifest = std.Io.Dir.cwd().readFileAlloc(io, manifest_path, b.allocator, .limited(64 * 1024)) catch return null;
|
|
return tsParseQuotedManifestValue(manifest, "\"version\"", "");
|
|
}
|
|
}
|
|
dir = std.fs.path.dirname(dir) orelse return null;
|
|
}
|
|
}
|
|
|
|
/// The version the SDK pins its aliased compiler to, read from the SDK
|
|
/// dependency's own packages/core/package.json — the same manifest the
|
|
/// CLI's gate reads, so both surfaces learn a version bump from one file.
|
|
fn tsPinnedCompilerVersion(b: *std.Build, sdk_root: []const u8) ?[]const u8 {
|
|
const manifest = std.Io.Dir.cwd().readFileAlloc(b.graph.io, b.pathJoin(&.{ sdk_root, "packages", "core", "package.json" }), b.allocator, .limited(64 * 1024)) catch return null;
|
|
const pin = tsParseQuotedManifestValue(manifest, "\"@typescript/old\"", "npm:typescript@") orelse return null;
|
|
// Exact X.Y.Z only (check-version-sync's pin shape), mirroring the
|
|
// twin's parseAliasedCompilerPin: a range is not a pin.
|
|
if (pin.len == 0) return null;
|
|
for (pin) |c| {
|
|
if (!std.ascii.isDigit(c) and c != '.') return null;
|
|
}
|
|
if (std.mem.count(u8, pin, ".") != 2) return null;
|
|
return pin;
|
|
}
|
|
|
|
/// Targeted scan for a manifest key's string value (the twin of
|
|
/// ts_core.zig's parsePackageVersion/parseAliasedCompilerPin scanners):
|
|
/// find `key`, expect `: "<required_prefix><value>"`, return `value`.
|
|
fn tsParseQuotedManifestValue(manifest_json: []const u8, comptime key: []const u8, comptime required_prefix: []const u8) ?[]const u8 {
|
|
const key_at = std.mem.indexOf(u8, manifest_json, key) orelse return null;
|
|
var rest = manifest_json[key_at + key.len ..];
|
|
rest = std.mem.trimStart(u8, rest, " \t\r\n");
|
|
if (rest.len == 0 or rest[0] != ':') return null;
|
|
rest = std.mem.trimStart(u8, rest[1..], " \t\r\n");
|
|
if (rest.len == 0 or rest[0] != '"') return null;
|
|
rest = rest[1..];
|
|
const end = std.mem.indexOfScalar(u8, rest, '"') orelse return null;
|
|
const value = rest[0..end];
|
|
if (!std.mem.startsWith(u8, value, required_prefix)) return null;
|
|
const suffix = value[required_prefix.len..];
|
|
if (suffix.len == 0) return null;
|
|
return suffix;
|
|
}
|
|
|
|
/// PR 166 adds both the published compile-cache bootstrap and the optional
|
|
/// library-profile optimization field. The installed bootstrap is therefore
|
|
/// the capability marker: keep older exact pins byte-for-byte compatible;
|
|
/// once that release is installed, Debug/native-dev gets `dev` and
|
|
/// release/package artifacts get `release` automatically.
|
|
fn scriptcProfileOptimization(b: *std.Build, dep: *std.Build.Dependency, optimize: std.builtin.OptimizeMode) ?[]const u8 {
|
|
const sdk_root = tsSdkRoot(b.allocator, b.graph.io, dep);
|
|
var dir: []const u8 = b.pathJoin(&.{ sdk_root, "packages", "core" });
|
|
while (true) {
|
|
if (!std.mem.eql(u8, std.fs.path.basename(dir), "node_modules")) {
|
|
const marker = b.pathJoin(&.{ dir, "node_modules", "scriptc", "dist", "bootstrap.js" });
|
|
std.Io.Dir.cwd().access(b.graph.io, marker, .{}) catch {
|
|
dir = std.fs.path.dirname(dir) orelse return null;
|
|
continue;
|
|
};
|
|
return if (optimize == .Debug) "dev" else "release";
|
|
}
|
|
dir = std.fs.path.dirname(dir) orelse return null;
|
|
}
|
|
}
|
|
|
|
/// The SDK dependency's real root, resolved the way both the toolchain
|
|
/// check and its teaching name it.
|
|
fn tsSdkRoot(allocator: std.mem.Allocator, io: std.Io, dep: *std.Build.Dependency) []const u8 {
|
|
const raw_root = dep.builder.build_root.path orelse ".";
|
|
return std.Io.Dir.cwd().realPathFileAlloc(io, raw_root, allocator) catch raw_root;
|
|
}
|
|
|
|
const TsToolingConsumer = enum { app_core, sqlite_schema };
|
|
|
|
/// Resolve the node + pinned TypeScript loader shared by the app-core
|
|
/// frontend and relational schema generator. Keep app-shape assertions out
|
|
/// of this helper: a Zig core may use SQLite without carrying a markup view.
|
|
fn tsToolingPreflight(b: *std.Build, dep: *std.Build.Dependency, consumer: TsToolingConsumer) []const u8 {
|
|
const node = b.findProgram(&.{"node"}, &.{}) catch switch (consumer) {
|
|
.app_core => @panic("\nbuilding a TypeScript app core needs node on PATH (the @native-sdk/core frontend checks the" ++
|
|
" core at build time; the binary you ship carries no JS runtime).\nInstall Node.js 24+" ++
|
|
" — https://nodejs.org or `brew install node` — and re-run.\n"),
|
|
.sqlite_schema => @panic("\nbuilding relational SQLite migrations needs node on PATH (the schema checker and" ++
|
|
" migration generator run at build time; the binary you ship carries no JS runtime).\nInstall Node.js" ++
|
|
" 24+ — https://nodejs.org or `brew install node` — and re-run.\n"),
|
|
};
|
|
switch (tsToolchainResolution(b, dep)) {
|
|
.resolved => {},
|
|
.unresolved => {
|
|
// Safety net for direct `zig build` users: the `native` CLI
|
|
// gates this itself, so reaching here means zig was invoked by
|
|
// hand against an SDK whose toolchain resolves nowhere. Name
|
|
// the SDK dependency's real location as a RESOLVED path and
|
|
// fail the configure phase cleanly — a teaching message, never
|
|
// a panic stack trace.
|
|
const sdk_root = tsSdkRoot(dep.builder.allocator, dep.builder.graph.io, dep);
|
|
switch (consumer) {
|
|
.app_core => std.debug.print(
|
|
\\
|
|
\\error: the @native-sdk/core frontend cannot resolve its TypeScript toolchain
|
|
\\(its compiler, @typescript/old). On a repo checkout, install it once with:
|
|
\\ cd {s}/packages/core && npm ci --include=dev
|
|
\\(An npm-installed @native-sdk/cli carries the toolchain automatically; if it
|
|
\\is missing there, the install is broken - reinstall @native-sdk/cli.)
|
|
\\
|
|
\\
|
|
, .{sdk_root}),
|
|
.sqlite_schema => std.debug.print(
|
|
\\
|
|
\\error: the relational SQLite schema generator cannot resolve its TypeScript toolchain
|
|
\\(its compiler, @typescript/old). On a repo checkout, install it once with:
|
|
\\ cd {s}/packages/core && npm ci --include=dev
|
|
\\(An npm-installed @native-sdk/cli carries the toolchain automatically; if it
|
|
\\is missing there, the install is broken - reinstall @native-sdk/cli.)
|
|
\\
|
|
\\
|
|
, .{sdk_root}),
|
|
}
|
|
std.process.exit(1);
|
|
},
|
|
.version_mismatch => |mismatch| {
|
|
// The wrong-VERSION shape gets its own teaching (the CLI's
|
|
// gate mirrors it): a conflicting consumer-tree
|
|
// @typescript/old shadows the SDK's exact pin, and no install
|
|
// command fixes what is already installed — the conflict has
|
|
// to move.
|
|
switch (consumer) {
|
|
.app_core => std.debug.print(
|
|
\\
|
|
\\error: the @native-sdk/core frontend's TypeScript compiler resolves at the
|
|
\\wrong version: @typescript/old resolves to typescript {s}, but the SDK pins
|
|
\\npm:typescript@{s}. Another package in this tree pins a conflicting
|
|
\\@typescript/old - align it with the SDK's pin (or remove it) and reinstall,
|
|
\\so the SDK's exact pin is the copy that resolves.
|
|
\\
|
|
\\
|
|
, .{ mismatch.resolved, mismatch.pinned }),
|
|
.sqlite_schema => std.debug.print(
|
|
\\
|
|
\\error: the relational SQLite schema generator's TypeScript compiler resolves at the
|
|
\\wrong version: @typescript/old resolves to typescript {s}, but the SDK pins
|
|
\\npm:typescript@{s}. Another package in this tree pins a conflicting
|
|
\\@typescript/old - align it with the SDK's pin (or remove it) and reinstall,
|
|
\\so the SDK's exact pin is the copy that resolves.
|
|
\\
|
|
\\
|
|
, .{ mismatch.resolved, mismatch.pinned }),
|
|
}
|
|
std.process.exit(1);
|
|
},
|
|
}
|
|
return node;
|
|
}
|
|
|
|
/// The TS-core-only shape gate plus the shared tooling preflight. The
|
|
/// frontend (the subset checker and contract-sidecar emitter) runs under
|
|
/// node; the compile itself is the external toolchain's.
|
|
fn tsCorePreflight(b: *std.Build, dep: *std.Build.Dependency, app_root: []const u8) []const u8 {
|
|
if (!appFileExists(b, app_root, "src/app.native")) {
|
|
@panic("\nthis app has a TypeScript core (src/core.ts) but no view: TS apps render markup," ++
|
|
" so add src/app.native (the whole view tier binds the core's model)\n");
|
|
}
|
|
return tsToolingPreflight(b, dep, .app_core);
|
|
}
|
|
|
|
/// The TypeScript-core compile lane: the frontend checks the core and
|
|
/// emits its contract sidecar, corewire projects the generated compile
|
|
/// entry and library-mode profile, the stager assembles the compile
|
|
/// tree (author sources with the mechanical staging transforms, the
|
|
/// staged SDK modules, the static compile surface), the exact-pinned
|
|
/// external toolchain builds the archive AND co-emits the archive's own
|
|
/// contract sidecar, and corewire generates the mirror module from THAT
|
|
/// document — so the boot identity fence always pairs the mirror with
|
|
/// its own compile. The staged module directory carries core.zig (the
|
|
/// mirror) + its staged runtime + main.zig, so the
|
|
/// generated wiring imports one fixed shape.
|
|
fn tsCoreStage(
|
|
b: *std.Build,
|
|
dep: *std.Build.Dependency,
|
|
target: std.Build.ResolvedTarget,
|
|
app_root: []const u8,
|
|
app_name: []const u8,
|
|
persist_capability: bool,
|
|
store_capability: bool,
|
|
relational_capability: bool,
|
|
credentials_capability: bool,
|
|
credentials_permission: bool,
|
|
persist_version: ?u64,
|
|
service_packages: []const ServicePackageConfig,
|
|
service_carrier_choice: ServiceCarrierOption,
|
|
service_pool_workers: ?u8,
|
|
build_trace: bool,
|
|
scriptc_optimization: ?[]const u8,
|
|
) TsCoreStage {
|
|
const node = tsCorePreflight(b, dep, app_root);
|
|
const window_views = collectTsWindowViews(b, app_root);
|
|
const has_services = appHasServiceFiles(b, app_root);
|
|
if (!scriptcCompileSupported(b.graph.host.result, target)) {
|
|
panicUnsupportedScriptcTarget(b, b.graph.host.result, target);
|
|
}
|
|
// Every TypeScript app compiles its core archive for the target. Any
|
|
// explicitly spelled Linux GNU target without a sufficient glibc version
|
|
// lands on Zig's too-old default floor, even when its triple matches the
|
|
// build host; reject it before either compile lane reaches the linker.
|
|
if (linuxGlibcSpellingHitsDefaultFloor(target)) {
|
|
panicScriptcLinuxGlibcSpelling(b, target);
|
|
}
|
|
const service_carrier = resolveServiceCarrier(service_carrier_choice, has_services, b, target);
|
|
|
|
// Relational schema analysis runs the real SQLite parser in memory before
|
|
// the ordinary core checker. Its generated SDK/static surfaces carry the
|
|
// named Cmd.q/Sub.q members, while the Zig projection embeds the exact
|
|
// append-only migration chain the checker accepted.
|
|
var checked_sdk_core: std.Build.LazyPath = dep.path("packages/core/sdk/core.ts");
|
|
var checked_static_core: std.Build.LazyPath = dep.path("packages/core/compile-surface/core.ts");
|
|
var migrations_zig: std.Build.LazyPath = dep.path("src/app_runner/no_migrations.zig");
|
|
if (relational_capability) {
|
|
const sqlite_check = b.addSystemCommand(&.{node});
|
|
sqlite_check.addFileArg(dep.path("build/ts_run.mjs"));
|
|
sqlite_check.addFileArg(dep.path("packages/core/src/sqlite_cli.ts"));
|
|
sqlite_check.addArg("--src");
|
|
sqlite_check.addDirectoryArg(b.path(appPath(b, app_root, "src")));
|
|
sqlite_check.addArg("--sdk-in");
|
|
sqlite_check.addFileArg(dep.path("packages/core/sdk/core.ts"));
|
|
sqlite_check.addArg("--static-in");
|
|
sqlite_check.addFileArg(dep.path("packages/core/compile-surface/core.ts"));
|
|
sqlite_check.addArg("--sdk-out");
|
|
checked_sdk_core = sqlite_check.addOutputFileArg("core.ts");
|
|
sqlite_check.addArg("--sdk-events-out");
|
|
_ = sqlite_check.addOutputFileArg("events.ts");
|
|
sqlite_check.addArg("--sdk-text-out");
|
|
_ = sqlite_check.addOutputFileArg("text.ts");
|
|
sqlite_check.addArg("--sdk-bytes-text-methods-out");
|
|
_ = sqlite_check.addOutputFileArg("bytes_text_methods.d.ts");
|
|
sqlite_check.addArg("--sdk-events-dts-out");
|
|
_ = sqlite_check.addOutputFileArg("events.d.ts");
|
|
sqlite_check.addArg("--sdk-text-dts-out");
|
|
_ = sqlite_check.addOutputFileArg("text.d.ts");
|
|
sqlite_check.addArg("--static-out");
|
|
checked_static_core = sqlite_check.addOutputFileArg("core_static.ts");
|
|
sqlite_check.addArg("--zig-out");
|
|
migrations_zig = sqlite_check.addOutputFileArg("migrations.zig");
|
|
sqlite_check.addArg("--metadata-out");
|
|
_ = sqlite_check.addOutputFileArg("sqlite.meta.json");
|
|
sqlite_check.addArgs(&.{ "--state", b.pathFromRoot(appPath(b, app_root, "src/schema/migrations.lock.json")) });
|
|
if (appFileExists(b, app_root, "src/schema/migrations.lock.json")) {
|
|
sqlite_check.addFileInput(b.path(appPath(b, app_root, "src/schema/migrations.lock.json")));
|
|
}
|
|
sqlite_check.addFileInput(dep.path("packages/core/src/sqlite_codegen.ts"));
|
|
sqlite_check.addFileInput(dep.path("packages/core/src/sqlite_runtime_policy.ts"));
|
|
for ([_][]const u8{ "events.ts", "text.ts", "bytes_text_methods.d.ts", "events.d.ts", "text.d.ts" }) |source| {
|
|
sqlite_check.addFileInput(dep.path(b.fmt("packages/core/sdk/{s}", .{source})));
|
|
}
|
|
addAppSqlDirInputs(b, sqlite_check, appPath(b, app_root, "src"));
|
|
}
|
|
|
|
// The frontend, in check-only mode: the subset checker and the
|
|
// contract sidecar, no emission. Every check-time teaching gates the
|
|
// compile here — the checker's NS diagnostics stream to stderr
|
|
// verbatim; nothing wraps them. The frontend runs through
|
|
// build/ts_run.mjs, not as `node cli.ts`: on the npm-installed
|
|
// layout the frontend's .ts sources live inside node_modules, where
|
|
// node refuses its builtin type stripping — the runner strips those
|
|
// modules with the frontend's own installed TypeScript and is a
|
|
// pass-through on a repo checkout. The frontend reads its own
|
|
// sources, the SDK modules, and the core's WHOLE import graph at
|
|
// run time; declare them all so an edit to ANY module of a
|
|
// multi-file core re-checks it (every .ts under src/ is a superset
|
|
// of the reachable imports: over-approximation only re-runs the
|
|
// check, never misses a stale input).
|
|
const check = b.addSystemCommand(&.{node});
|
|
check.setName("native ts frontend (core + service contracts)");
|
|
check.addFileArg(dep.path("build/ts_run.mjs"));
|
|
check.addFileArg(dep.path("packages/core/src/cli.ts"));
|
|
check.addFileArg(b.path(appPath(b, app_root, "src/core.ts")));
|
|
check.addArg("--contract");
|
|
const contract_raw = check.addOutputFileArg("core.contract.json");
|
|
const contract = stabilizeGeneratedFile(b, contract_raw, "core.contract.json", build_trace);
|
|
// The document's entry spelling is app-relative (the sidecar/facade
|
|
// contract carries no machine paths).
|
|
check.addArgs(&.{ "--contract-entry", "src/core.ts" });
|
|
const services_contract: ?std.Build.LazyPath = if (has_services) services_contract: {
|
|
check.addArg("--services-contract");
|
|
const raw = check.addOutputFileArg("services.contract.json");
|
|
break :services_contract stabilizeGeneratedFile(b, raw, "services.contract.json", build_trace);
|
|
} else null;
|
|
if (persist_capability) check.addArgs(&.{ "--capability", "persist" });
|
|
for (service_packages) |package_entry| {
|
|
check.addArgs(&.{
|
|
"--service-package",
|
|
b.fmt("{s}|{s}|{s}", .{ package_entry.name, package_entry.version, package_entry.content_hash }),
|
|
});
|
|
}
|
|
if (store_capability) check.addArgs(&.{ "--capability", "store" });
|
|
if (relational_capability) check.addArgs(&.{ "--capability", "sqlite" });
|
|
if (credentials_capability) check.addArgs(&.{ "--capability", "credentials" });
|
|
if (credentials_permission) check.addArgs(&.{ "--permission", "credentials" });
|
|
// Activate the cross-tier registry check even when discovery found no
|
|
// roots: the empty set must reject a core that declares a window.
|
|
check.addArg("--window-views");
|
|
for (window_views.views) |view| check.addArgs(&.{ "--window-view", view.label });
|
|
if (relational_capability) {
|
|
check.addArg("--sdk-core");
|
|
check.addFileArg(checked_sdk_core);
|
|
}
|
|
if (persist_version) |version| {
|
|
check.addArgs(&.{ "--persist-version", b.fmt("{d}", .{version}) });
|
|
check.addArgs(&.{ "--persist-state", appPath(b, app_root, ".native/cache/persist-schema.json") });
|
|
}
|
|
addTsDirInputs(b, dep.builder, check, "packages/core/sdk");
|
|
addAppTsDirInputs(b, check, appPath(b, app_root, "src"));
|
|
for (frontend_sources) |source| {
|
|
check.addFileInput(dep.path(b.fmt("packages/core/src/{s}", .{source})));
|
|
}
|
|
// Service contracts echo the exact scriptc pin read from this manifest.
|
|
// Declare that runtime read explicitly so a compiler bump invalidates a
|
|
// warm build cache before the compile lane checks the echoed version.
|
|
if (has_services) check.addFileInput(dep.path("packages/core/package.json"));
|
|
|
|
// corewire, compiled from the SDK dependency for the build host: one
|
|
// invocation projects the generated compile entry and its profile,
|
|
// so the profile's entry spelling and the facade file can never skew.
|
|
const corewire_exe = corewireExe(b, dep);
|
|
const project = b.addRunArtifact(corewire_exe);
|
|
project.setName("native corewire (core facade + profile)");
|
|
project.addArg("--sidecar");
|
|
project.addFileArg(contract);
|
|
project.addArg("--facade");
|
|
const facade = project.addOutputFileArg("core_facade.ts");
|
|
project.addArg("--profile");
|
|
const profile = project.addOutputFileArg("core_profile.json");
|
|
if (scriptc_optimization) |value| project.addArgs(&.{ "--optimization", value });
|
|
|
|
// The fourth corewire projection is intentionally driven only by the
|
|
// service sidecar: the generated TypeScript dispatch loop and Zig
|
|
// name/index registry never rediscover facts from author source.
|
|
var service_registry: std.Build.LazyPath = dep.path("src/app_runner/no_services.zig");
|
|
var service_exe: ?std.Build.LazyPath = null;
|
|
var service_archive: ?std.Build.LazyPath = null;
|
|
var service_client: ?std.Build.LazyPath = null;
|
|
if (services_contract) |service_contract| {
|
|
const service_project = b.addRunArtifact(corewire_exe);
|
|
service_project.setName("native corewire (service host + ABI projections)");
|
|
service_project.addArg("--services-sidecar");
|
|
service_project.addFileArg(service_contract);
|
|
service_project.addArg("--service-host-main");
|
|
const service_host_main = service_project.addOutputFileArg("service_host_main.ts");
|
|
service_project.addArg("--service-registry");
|
|
const service_registry_raw = service_project.addOutputFileArg("services.zig");
|
|
service_registry = stabilizeGeneratedFile(b, service_registry_raw, "services.zig", build_trace);
|
|
service_project.addArg("--service-client");
|
|
const service_client_raw = service_project.addOutputFileArg("services.gen.ts");
|
|
service_client = stabilizeGeneratedFile(b, service_client_raw, "services.gen.ts", build_trace);
|
|
const service_inproc_main: ?std.Build.LazyPath = if (service_carrier == .in_process) inproc: {
|
|
service_project.addArg("--service-inproc-main");
|
|
break :inproc service_project.addOutputFileArg("service_inproc_main.ts");
|
|
} else null;
|
|
const service_inproc_profile: ?std.Build.LazyPath = if (service_carrier == .in_process) inproc: {
|
|
service_project.addArg("--service-inproc-profile");
|
|
break :inproc service_project.addOutputFileArg("service_profile.json");
|
|
} else null;
|
|
|
|
// Ordinary service TypeScript is staged without core-subset rewrites.
|
|
// The one service-boundary lowering turns NS1067's `{ kind, message }`
|
|
// throw into the tagged Error shape the pinned compiler can catch from an
|
|
// imported op; no deterministic profile fences participate here.
|
|
const service_stage_run = b.addSystemCommand(&.{node});
|
|
service_stage_run.setName("native stage TypeScript services");
|
|
service_stage_run.addFileArg(dep.path("packages/core/scripts/stage_external_services.mjs"));
|
|
service_stage_run.addArg("--src");
|
|
service_stage_run.addDirectoryArg(b.path(appPath(b, app_root, "src")));
|
|
service_stage_run.addArg("--host-main");
|
|
service_stage_run.addFileArg(service_host_main);
|
|
if (service_inproc_main) |inproc_main| {
|
|
service_stage_run.addArg("--inproc-main");
|
|
service_stage_run.addFileArg(inproc_main);
|
|
}
|
|
if (service_inproc_profile) |inproc_profile| {
|
|
service_stage_run.addArg("--inproc-profile");
|
|
service_stage_run.addFileArg(inproc_profile);
|
|
}
|
|
if (scriptc_optimization) |value| service_project.addArgs(&.{ "--optimization", value });
|
|
service_stage_run.addArg("--contract");
|
|
service_stage_run.addFileArg(service_contract);
|
|
service_stage_run.addArg("--out");
|
|
const service_stage_dir = service_stage_run.addOutputDirectoryArg("services-stage");
|
|
// The staging script discovers nested service modules recursively;
|
|
// a directory LazyPath alone does not hash those discovered files.
|
|
service_stage_run.has_side_effects = true;
|
|
|
|
const service_compile = b.addSystemCommand(&.{node});
|
|
service_compile.setName("native scriptc service compile");
|
|
if (build_trace) service_compile.setEnvironmentVariable("SCRIPTC_TIMING", "1");
|
|
service_compile.addFileArg(dep.path("packages/core/scripts/run_external_service_compiler.mjs"));
|
|
service_compile.addFileInput(dep.path("packages/core/scripts/compiler_command.mjs"));
|
|
service_compile.addArg("--stage");
|
|
service_compile.addDirectoryArg(service_stage_dir);
|
|
service_compile.addArg("--manifest");
|
|
service_compile.addFileArg(dep.path("packages/core/package.json"));
|
|
service_compile.addArg("--contract");
|
|
service_compile.addFileArg(service_contract);
|
|
if (service_carrier == .in_process) {
|
|
// The in-process carrier links the service class into the app
|
|
// binary; no sibling executable is built or packaged.
|
|
service_compile.addArg("--out-archive");
|
|
service_archive = service_compile.addOutputFileArg(b.fmt("lib{s}_services.a", .{externalCoreSymbolName(b, app_name)}));
|
|
} else {
|
|
service_compile.addArg("--out-exe");
|
|
const service_suffix = if (target.result.os.tag == .windows) ".exe" else "";
|
|
service_exe = service_compile.addOutputFileArg(b.fmt("{s}_services{s}", .{ app_name, service_suffix }));
|
|
}
|
|
service_compile.addArgs(&.{
|
|
"--host-platform",
|
|
b.fmt("{t}-{t}-{t}", .{ b.graph.host.result.cpu.arch, b.graph.host.result.os.tag, b.graph.host.result.abi }),
|
|
"--target-platform",
|
|
scriptcPlatformTriple(b, target),
|
|
// Cross service compiles run the pinned compiler's zig-cc
|
|
// lane; hand over this build's own zig so the lane never
|
|
// depends on a PATH zig (native compiles ignore it).
|
|
"--zig-exe",
|
|
b.graph.zig_exe,
|
|
});
|
|
addScriptcAndroidNdk(b, service_compile, target);
|
|
if (b.graph.environ_map.get("NATIVE_SDK_CORE_COMPILER")) |override| {
|
|
service_compile.addArgs(&.{ "--compiler", override });
|
|
} else {
|
|
// Let Node resolve the dependency from the SDK package origin:
|
|
// this is npm's own nested/hoisted/global-sibling walk, and the
|
|
// driver follows scriptc's published `bin` declaration.
|
|
service_compile.addArg("--compiler-package-origin");
|
|
service_compile.addFileArg(dep.path("packages/core/package.json"));
|
|
}
|
|
}
|
|
|
|
// The compile stage: author sources + staged SDK + static surface +
|
|
// generated entry/profile, one scratch tree.
|
|
const stage_run = b.addSystemCommand(&.{node});
|
|
stage_run.setName("native stage TypeScript core");
|
|
stage_run.addFileArg(dep.path("packages/core/scripts/stage_external_core.mjs"));
|
|
stage_run.addArg("--src");
|
|
stage_run.addDirectoryArg(b.path(appPath(b, app_root, "src")));
|
|
stage_run.addArg("--sdk");
|
|
stage_run.addDirectoryArg(dep.path("packages/core/sdk"));
|
|
stage_run.addArg("--static");
|
|
stage_run.addFileArg(checked_static_core);
|
|
stage_run.addArg("--facade");
|
|
stage_run.addFileArg(facade);
|
|
stage_run.addArg("--profile");
|
|
stage_run.addFileArg(profile);
|
|
if (service_client) |client| {
|
|
stage_run.addArg("--services-client");
|
|
stage_run.addFileArg(client);
|
|
}
|
|
// `addDirectoryArg` orders the stager after a generated directory but
|
|
// does not hash a source directory's discovered contents. The old graph
|
|
// accidentally got core-source invalidation from changing producer cache
|
|
// paths. Now that generated ABI files are stabilized by content, state
|
|
// every source the stager copies explicitly: authored core files plus the
|
|
// two SDK implementation modules. Services are excluded because they have
|
|
// their own compile lane.
|
|
addAppCoreTsDirInputs(b, stage_run, appPath(b, app_root, "src"));
|
|
addStagedCoreSdkInputs(b, dep.builder, stage_run);
|
|
stage_run.addArg("--out");
|
|
const stage_dir = stage_run.addOutputDirectoryArg("stage");
|
|
|
|
// The external compile itself: driver-verified against the SDK's
|
|
// exact pin, archive normalized, the co-emitted sidecar captured.
|
|
const symbol_name = externalCoreSymbolName(b, app_name);
|
|
const compile = b.addSystemCommand(&.{node});
|
|
compile.setName("native scriptc core compile");
|
|
if (build_trace) compile.setEnvironmentVariable("SCRIPTC_TIMING", "1");
|
|
compile.addFileArg(dep.path("packages/core/scripts/run_external_core_compiler.mjs"));
|
|
compile.addFileInput(dep.path("packages/core/scripts/compiler_command.mjs"));
|
|
compile.addArg("--stage");
|
|
compile.addDirectoryArg(stage_dir);
|
|
compile.addArgs(&.{ "--name", symbol_name });
|
|
compile.addArg("--manifest");
|
|
compile.addFileArg(dep.path("packages/core/package.json"));
|
|
compile.addArg("--frontend-sidecar");
|
|
compile.addFileArg(contract);
|
|
compile.addArg("--out-archive");
|
|
const archive = compile.addOutputFileArg(b.fmt("lib{s}.a", .{symbol_name}));
|
|
compile.addArg("--out-sidecar");
|
|
const compiled_sidecar = compile.addOutputFileArg("core.contract.json");
|
|
compile.addArgs(&.{
|
|
"--host-platform",
|
|
b.fmt("{t}-{t}-{t}", .{ b.graph.host.result.cpu.arch, b.graph.host.result.os.tag, b.graph.host.result.abi }),
|
|
"--target-platform",
|
|
scriptcPlatformTriple(b, target),
|
|
// Keep the core and service archives on the same cross compiler and
|
|
// target. Native compiles ignore the supplied Zig path.
|
|
"--zig-exe",
|
|
b.graph.zig_exe,
|
|
});
|
|
addScriptcAndroidNdk(b, compile, target);
|
|
if (b.graph.environ_map.get("NATIVE_SDK_CORE_COMPILER")) |override| {
|
|
// The development override: point at any toolchain command; the
|
|
// driver still refuses a release other than the SDK's pin.
|
|
compile.addArgs(&.{ "--compiler", override });
|
|
} else {
|
|
compile.addArg("--compiler-package-origin");
|
|
compile.addFileArg(dep.path("packages/core/package.json"));
|
|
}
|
|
|
|
// The mirror, generated from the archive's OWN co-emitted contract.
|
|
const mirror = b.addRunArtifact(corewire_exe);
|
|
mirror.setName("native corewire (compiled-core mirror)");
|
|
mirror.addArg("--sidecar");
|
|
mirror.addFileArg(compiled_sidecar);
|
|
mirror.addArg("--out");
|
|
const shim = mirror.addOutputFileArg("core_shim.zig");
|
|
|
|
// The staged module directory: the mirror is the app's core.zig,
|
|
// and it imports its staged shim runtime relatively.
|
|
const staged = b.addWriteFiles();
|
|
_ = staged.addCopyFile(shim, "core.zig");
|
|
_ = staged.addCopyFile(dep.path("tools/corewire/shim_rt.zig"), "shim_rt.zig");
|
|
_ = staged.addCopyFile(dep.path("tools/corewire/core_abi.zig"), "core_abi.zig");
|
|
_ = staged.addCopyFile(service_registry, "services.zig");
|
|
// The carrier selection, as one staged constant module: the generated
|
|
// wiring comptime-switches its service transport on it.
|
|
_ = staged.add("service_carrier.zig", b.fmt(
|
|
\\//! Generated by the app build: the resolved service-carrier selection.
|
|
\\pub const Kind = enum {{ none, child, in_process }};
|
|
\\pub const kind: Kind = .{t};
|
|
\\/// Pool width for the in-process carrier; null resolves the
|
|
\\/// runtime default (min(4, cores)).
|
|
\\pub const pool_workers: ?usize = {?d};
|
|
\\
|
|
, .{ service_carrier, service_pool_workers }));
|
|
_ = staged.addCopyFile(migrations_zig, "migrations.zig");
|
|
// Primary markup bytes live in their own tiny C translation unit. They
|
|
// no longer participate in Zig source analysis, so a markup edit reuses
|
|
// the compiled app/SDK graph and performs only C data compile + relink.
|
|
const markup_embed = b.addSystemCommand(&.{node});
|
|
markup_embed.setName("native embed primary markup data");
|
|
markup_embed.addFileArg(dep.path("packages/core/scripts/embed_markup_c.mjs"));
|
|
markup_embed.addFileArg(b.path(appPath(b, app_root, "src/app.native")));
|
|
const markup_c = markup_embed.addOutputFileArg("app_markup.c");
|
|
for (window_views.sources) |source| {
|
|
_ = staged.addCopyFile(b.path(appPath(b, app_root, source.source_path)), source.staged_path);
|
|
}
|
|
_ = staged.add("window_views.zig", tsWindowRegistrySource(b, window_views));
|
|
const main_root = staged.addCopyFile(dep.path("src/app_runner/ts_core_main.zig"), "main.zig");
|
|
// The mobile wiring stages beside the desktop entry: same mirror, same
|
|
// registry, same carrier constant — only the shell differs (the embed
|
|
// host's AppDef contract instead of a process `main`).
|
|
const mobile_root = staged.addCopyFile(dep.path("src/app_runner/ts_core_mobile.zig"), "mobile.zig");
|
|
return .{
|
|
.main_root = main_root,
|
|
.mobile_root = mobile_root,
|
|
.archive = archive,
|
|
.service_exe = service_exe,
|
|
.service_archive = service_archive,
|
|
.markup_c = markup_c,
|
|
.migrations = migrations_zig,
|
|
};
|
|
}
|
|
|
|
fn sqliteMigrationsStage(b: *std.Build, dep: *std.Build.Dependency, app_root: []const u8) std.Build.LazyPath {
|
|
const node = tsToolingPreflight(b, dep, .sqlite_schema);
|
|
const generate = b.addSystemCommand(&.{node});
|
|
generate.addFileArg(dep.path("build/ts_run.mjs"));
|
|
generate.addFileArg(dep.path("packages/core/src/sqlite_cli.ts"));
|
|
generate.addArg("--src");
|
|
generate.addDirectoryArg(b.path(appPath(b, app_root, "src")));
|
|
generate.addArg("--zig-out");
|
|
const migrations = generate.addOutputFileArg("migrations.zig");
|
|
generate.addArgs(&.{ "--state", b.pathFromRoot(appPath(b, app_root, "src/schema/migrations.lock.json")) });
|
|
if (appFileExists(b, app_root, "src/schema/migrations.lock.json")) {
|
|
generate.addFileInput(b.path(appPath(b, app_root, "src/schema/migrations.lock.json")));
|
|
}
|
|
generate.addFileInput(dep.path("packages/core/src/sqlite_codegen.ts"));
|
|
generate.addFileInput(dep.path("packages/core/src/sqlite_runtime_policy.ts"));
|
|
addAppSqlDirInputs(b, generate, appPath(b, app_root, "src"));
|
|
return migrations;
|
|
}
|
|
|
|
fn addAppSqlDirInputs(b: *std.Build, run: *std.Build.Step.Run, src_path: []const u8) void {
|
|
var dir = b.build_root.handle.openDir(b.graph.io, src_path, .{ .iterate = true }) catch return;
|
|
defer dir.close(b.graph.io);
|
|
var walker = dir.walk(b.allocator) catch return;
|
|
defer walker.deinit();
|
|
while (walker.next(b.graph.io) catch null) |entry| {
|
|
if (entry.kind != .file or !std.mem.endsWith(u8, entry.basename, ".sql")) continue;
|
|
run.addFileInput(b.path(b.fmt("{s}/{s}", .{ src_path, entry.path })));
|
|
}
|
|
}
|
|
|
|
/// corewire (the contract-sidecar shim generator), compiled from the SDK
|
|
/// dependency's sources for the build host.
|
|
fn corewireExe(b: *std.Build, dep: *std.Build.Dependency) *std.Build.Step.Compile {
|
|
const mod = b.createModule(.{
|
|
.root_source_file = dep.path("tools/corewire/main.zig"),
|
|
.target = b.graph.host,
|
|
.optimize = .Debug,
|
|
});
|
|
return b.addExecutable(.{
|
|
.name = "corewire",
|
|
.root_module = mod,
|
|
.use_llvm = useLlvmWorkaround(b.graph.host),
|
|
});
|
|
}
|
|
|
|
/// The archive's symbol-safe stem (`<app>_core` with every non-identifier
|
|
/// byte folded to '_'), the -o name the external compile builds under.
|
|
fn externalCoreSymbolName(b: *std.Build, app_name: []const u8) []const u8 {
|
|
const stem = b.fmt("{s}_core", .{app_name});
|
|
const sanitized = b.dupe(stem);
|
|
for (sanitized) |*char| {
|
|
const ok = (char.* >= 'a' and char.* <= 'z') or (char.* >= 'A' and char.* <= 'Z') or
|
|
(char.* >= '0' and char.* <= '9') or char.* == '_';
|
|
if (!ok) char.* = '_';
|
|
}
|
|
return sanitized;
|
|
}
|
|
|
|
/// Declare every .ts file in an SDK-relative directory as a file input of
|
|
/// the transpile step (the SDK library modules an app may import).
|
|
fn addTsDirInputs(b: *std.Build, sdk_builder: *std.Build, transpile: *std.Build.Step.Run, dir_path: []const u8) void {
|
|
var dir = sdk_builder.build_root.handle.openDir(b.graph.io, dir_path, .{ .iterate = true }) catch return;
|
|
defer dir.close(b.graph.io);
|
|
var it = dir.iterate();
|
|
while (it.next(b.graph.io) catch null) |entry| {
|
|
if (entry.kind != .file) continue;
|
|
if (!std.mem.endsWith(u8, entry.name, ".ts")) continue;
|
|
transpile.addFileInput(sdk_builder.path(b.fmt("{s}/{s}", .{ dir_path, entry.name })));
|
|
}
|
|
}
|
|
|
|
/// Declare every .ts file under the app's src/ (recursively — a core may
|
|
/// split into subdirectories) as a file input of the transpile step.
|
|
fn addAppTsDirInputs(b: *std.Build, transpile: *std.Build.Step.Run, src_path: []const u8) void {
|
|
var dir = b.build_root.handle.openDir(b.graph.io, src_path, .{ .iterate = true }) catch return;
|
|
defer dir.close(b.graph.io);
|
|
var walker = dir.walk(b.allocator) catch return;
|
|
defer walker.deinit();
|
|
while (walker.next(b.graph.io) catch null) |entry| {
|
|
if (entry.kind != .file) continue;
|
|
if (!std.mem.endsWith(u8, entry.basename, ".ts")) continue;
|
|
transpile.addFileInput(b.path(b.fmt("{s}/{s}", .{ src_path, entry.path })));
|
|
}
|
|
}
|
|
|
|
/// Declare exactly the author TypeScript files copied into the core compiler's
|
|
/// scratch tree. `src/services/` is a separate compiler class and `.d.ts`
|
|
/// files are declarations for editor/provider use, not scriptc source inputs.
|
|
fn addAppCoreTsDirInputs(b: *std.Build, stage: *std.Build.Step.Run, src_path: []const u8) void {
|
|
var dir = b.build_root.handle.openDir(b.graph.io, src_path, .{ .iterate = true }) catch return;
|
|
defer dir.close(b.graph.io);
|
|
var walker = dir.walk(b.allocator) catch return;
|
|
defer walker.deinit();
|
|
while (walker.next(b.graph.io) catch null) |entry| {
|
|
if (entry.kind != .file or !std.mem.endsWith(u8, entry.basename, ".ts") or std.mem.endsWith(u8, entry.basename, ".d.ts")) continue;
|
|
const normalized = b.dupe(entry.path);
|
|
for (normalized) |*char| if (char.* == '\\') {
|
|
char.* = '/';
|
|
};
|
|
if (std.mem.startsWith(u8, normalized, "services/")) continue;
|
|
stage.addFileInput(b.path(b.fmt("{s}/{s}", .{ src_path, entry.path })));
|
|
}
|
|
}
|
|
|
|
/// Declare the SDK implementation modules copied by
|
|
/// stage_external_core.mjs. A directory LazyPath supplies ordering only for
|
|
/// this source tree; these file inputs make implementation-only SDK edits
|
|
/// invalidate the staged tree and compiled core archive even when the public
|
|
/// contract remains byte-identical. Public for the repository fixture graph,
|
|
/// which exercises the same compiler boundary as app builds.
|
|
pub fn addStagedCoreSdkInputs(b: *std.Build, sdk_builder: *std.Build, stage: *std.Build.Step.Run) void {
|
|
for ([_][]const u8{ "text.ts", "events.ts" }) |source| {
|
|
stage.addFileInput(sdk_builder.path(b.fmt("packages/core/sdk/{s}", .{source})));
|
|
}
|
|
}
|
|
|
|
fn appHasServiceFiles(b: *std.Build, app_root: []const u8) bool {
|
|
const services_path = appPath(b, app_root, "src/services");
|
|
var dir = b.build_root.handle.openDir(b.graph.io, services_path, .{ .iterate = true }) catch return false;
|
|
defer dir.close(b.graph.io);
|
|
var walker = dir.walk(b.allocator) catch return false;
|
|
defer walker.deinit();
|
|
while (walker.next(b.graph.io) catch null) |entry| {
|
|
if (entry.kind == .file and std.mem.endsWith(u8, entry.basename, ".ts") and
|
|
!std.mem.endsWith(u8, entry.basename, ".d.ts")) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// The `native_sdk_app_*` C ABI every embed static library exports.
|
|
pub const mobile_export_symbol_names = [_][]const u8{
|
|
"native_sdk_app_create",
|
|
"native_sdk_app_destroy",
|
|
"native_sdk_app_destroy_with_status",
|
|
"native_sdk_app_start",
|
|
"native_sdk_app_activate",
|
|
"native_sdk_app_deactivate",
|
|
"native_sdk_app_stop",
|
|
"native_sdk_app_resize",
|
|
"native_sdk_app_viewport",
|
|
"native_sdk_app_viewport_state",
|
|
"native_sdk_app_gpu_frame_state",
|
|
"native_sdk_app_text_input_state",
|
|
"native_sdk_app_set_text_measure",
|
|
"native_sdk_app_set_audio_service",
|
|
"native_sdk_app_set_credential_service",
|
|
"native_sdk_app_audio_event",
|
|
"native_sdk_app_set_image_service",
|
|
"native_sdk_app_set_automation_dir",
|
|
"native_sdk_app_set_data_root",
|
|
"native_sdk_app_touch",
|
|
"native_sdk_app_scroll",
|
|
"native_sdk_app_key",
|
|
"native_sdk_app_text",
|
|
"native_sdk_app_ime",
|
|
"native_sdk_app_command",
|
|
"native_sdk_app_frame",
|
|
"native_sdk_app_chrome_tab_count",
|
|
"native_sdk_app_chrome_tab_at",
|
|
"native_sdk_app_chrome_primary_action",
|
|
"native_sdk_app_chrome_selected_tab",
|
|
"native_sdk_app_chrome_navigation_depth",
|
|
"native_sdk_app_chrome_navigation_back_command",
|
|
"native_sdk_app_chrome_icon_pixels",
|
|
"native_sdk_app_set_form_factor",
|
|
"native_sdk_app_set_chrome_tabs_projected",
|
|
"native_sdk_app_set_asset_root",
|
|
"native_sdk_app_set_asset_entry",
|
|
"native_sdk_app_last_command_count",
|
|
"native_sdk_app_last_command_name",
|
|
"native_sdk_app_last_error_name",
|
|
"native_sdk_app_widget_semantics_count",
|
|
"native_sdk_app_widget_semantics_at",
|
|
"native_sdk_app_widget_semantics_by_id",
|
|
"native_sdk_app_widget_text_geometry",
|
|
"native_sdk_app_widget_action",
|
|
"native_sdk_app_render_pixel_size",
|
|
"native_sdk_app_render_pixels",
|
|
"native_sdk_app_render_pixels_damage",
|
|
};
|
|
|
|
pub const MobileSceneOption = enum {
|
|
/// The user app's UiApp on a gpu_surface view (window 1,
|
|
/// "mobile-surface"), pumped by the host's frame callback.
|
|
canvas,
|
|
/// The fixed WebView shell the ios/android/mobile-shell examples embed
|
|
/// today; the app module is not compiled in.
|
|
webview,
|
|
};
|
|
|
|
pub const MobileLibOptions = struct {
|
|
name: []const u8,
|
|
/// Mobile app entry (the `"app"` module the embed host drives); must
|
|
/// declare `Model`, `Msg`, `initModel`, and `mobileOptions` — see
|
|
/// `src/embed/ui_host.zig`. Ignored for `.scene = .webview`.
|
|
main: []const u8 = "src/main.zig",
|
|
scene: MobileSceneOption = .canvas,
|
|
/// Link and install the engine-owned Tier-2 record store. Standard
|
|
/// `addApp` builds infer this from app.zon; direct `addMobileLib`
|
|
/// callers state it here because that lower-level API has no manifest.
|
|
store_capability: bool = false,
|
|
/// Link and install the engine-owned Tier-3 relational database.
|
|
relational_capability: bool = false,
|
|
/// Generated append-only migration module. Direct low-level callers may
|
|
/// omit it and open an empty version-0 database.
|
|
relational_migrations: ?std.Build.LazyPath = null,
|
|
/// Compile the Tier-4 core credential effects into the mobile host.
|
|
credentials_capability: bool = false,
|
|
/// Grant those effects access to the registered OS credential service.
|
|
credentials_permission: bool = false,
|
|
/// Grant raw file effects access outside the OS-owned app data root.
|
|
filesystem_permission: bool = false,
|
|
/// Stable app identity used as the Keychain/Keystore service namespace.
|
|
credentials_service: []const u8 = "dev.native_sdk.app",
|
|
/// Frozen registered-image pixel budget. Standard app builds infer it
|
|
/// from app.zon; low-level embedders default to the SDK's 1 MiB tier.
|
|
max_image_pixel_bytes: usize = 1024 * 1024,
|
|
/// A TypeScript core's staged mobile wiring: set by `addAppArtifacts`
|
|
/// when the tree carries src/core.ts. The `app` module roots at the
|
|
/// staged mobile entry instead of `main`, and the compiled core (and
|
|
/// in-process service) archives merge into the embed static library —
|
|
/// the host tiers keep linking exactly one archive.
|
|
ts_core: ?MobileTsCore = null,
|
|
};
|
|
|
|
/// The TypeScript pieces a mobile embed library consumes (see
|
|
/// `MobileLibOptions.ts_core`).
|
|
pub const MobileTsCore = struct {
|
|
/// The staged mobile wiring (mobile.zig beside the generated mirror).
|
|
main_root: std.Build.LazyPath,
|
|
/// The compiled-core archive; merged into the embed library.
|
|
archive: std.Build.LazyPath,
|
|
/// The in-process service archive, when src/services exists.
|
|
service_archive: ?std.Build.LazyPath,
|
|
markup_c: std.Build.LazyPath,
|
|
/// The app.zon module the mobile wiring reads scene chrome, identity,
|
|
/// and theme from (`app_manifest_zon`).
|
|
manifest_mod: *std.Build.Module,
|
|
};
|
|
|
|
/// Mobile counterpart of `addApp`: produce the embed static library
|
|
/// (`native_sdk_app_*` C ABI) compiled with the user's UiApp. Call it from
|
|
/// a standalone build.zig (it registers the standard `target`/`optimize`
|
|
/// options itself).
|
|
pub fn addMobileLib(b: *std.Build, dep: *std.Build.Dependency, options: MobileLibOptions) void {
|
|
const target = nativeSdkTarget(b);
|
|
const optimize_request = b.option(std.builtin.OptimizeMode, "optimize", "Prioritize performance, safety, or binary size");
|
|
const optimize = exampleOptimizeMode(b, optimize_request, .Debug);
|
|
addMobileLibWithTarget(b, dep, target, optimize, options);
|
|
}
|
|
|
|
/// The mobile-lib wiring behind `addMobileLib`, for builds that already
|
|
/// resolved `target`/`optimize` (`addAppArtifacts` registers the `lib`
|
|
/// step through this for iOS/Android targets, so every standard app —
|
|
/// generated graph or ejected `addApp` — can produce the embed library
|
|
/// with nothing but `-Dtarget`).
|
|
fn addMobileLibWithTarget(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, options: MobileLibOptions) void {
|
|
const native_sdk_mod = nativeSdkModule(b, dep, target, optimize);
|
|
// Android hosts load the embed lib inside a shared object
|
|
// (System.loadLibrary / NativeActivity), so every object must be PIC —
|
|
// without it Zig emits local-exec TLS relocations (R_AARCH64_TLSLE_*)
|
|
// that the NDK linker rejects when producing the shim .so. Imported
|
|
// modules leave `pic` null and inherit this from the root module.
|
|
const pic: ?bool = if (target.result.abi.isAndroid()) true else null;
|
|
const exports_mod = b.createModule(.{
|
|
.root_source_file = dep.path(switch (options.scene) {
|
|
.canvas => "src/embed/app_exports.zig",
|
|
.webview => "src/embed/c_exports.zig",
|
|
}),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.pic = pic,
|
|
});
|
|
exports_mod.addImport("native_sdk", native_sdk_mod);
|
|
if (options.scene == .canvas) {
|
|
const mobile_options = b.addOptions();
|
|
mobile_options.addOption(bool, "store_capability", options.store_capability);
|
|
mobile_options.addOption(bool, "relational_capability", options.relational_capability);
|
|
mobile_options.addOption(bool, "credentials_capability", options.credentials_capability);
|
|
mobile_options.addOption(bool, "credentials_permission", options.credentials_permission);
|
|
mobile_options.addOption(bool, "filesystem_permission", options.filesystem_permission);
|
|
mobile_options.addOption([]const u8, "credentials_service", options.credentials_service);
|
|
mobile_options.addOption(usize, "max_image_pixel_bytes", options.max_image_pixel_bytes);
|
|
exports_mod.addImport("mobile_build_options", mobile_options.createModule());
|
|
const migration_path = options.relational_migrations orelse dep.path("src/app_runner/no_migrations.zig");
|
|
const migration_mod = b.createModule(.{ .root_source_file = migration_path, .target = target, .optimize = optimize });
|
|
migration_mod.addImport("native_sdk", native_sdk_mod);
|
|
exports_mod.addImport("relational_migrations", migration_mod);
|
|
// A TypeScript core's app module roots at the staged mobile wiring;
|
|
// a Zig core's at the app's own mobile entry. Either way the embed
|
|
// host sees the same AppDef contract (Model/Msg/initModel/
|
|
// mobileOptions).
|
|
const app_mod = if (options.ts_core) |ts| ts_app: {
|
|
const mod = b.createModule(.{
|
|
.root_source_file = ts.main_root,
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
mod.addImport("app_manifest_zon", ts.manifest_mod);
|
|
break :ts_app mod;
|
|
} else localModule(b, target, optimize, options.main);
|
|
app_mod.addImport("native_sdk", native_sdk_mod);
|
|
exports_mod.addImport("app", app_mod);
|
|
}
|
|
if (options.ts_core) |ts| {
|
|
// The compiled TypeScript archives merge into the embed static
|
|
// library (Zig's static-lib emission bundles archive inputs), so
|
|
// the iOS/Android host tiers keep linking the one archive they
|
|
// already stage. The toolchain's runtime needs libc; the host
|
|
// link supplies it (plus -lm/-ldl on Android, which the Android
|
|
// host link already passes).
|
|
exports_mod.link_libc = true;
|
|
exports_mod.addObjectFile(ts.archive);
|
|
exports_mod.addObjectFile(markupDataObject(b, target, optimize, ts.markup_c).getEmittedBin());
|
|
if (ts.service_archive) |service_archive| exports_mod.addObjectFile(service_archive);
|
|
}
|
|
if (options.store_capability or options.relational_capability) {
|
|
exports_mod.addIncludePath(dep.path("third_party/sqlite"));
|
|
exports_mod.addCSourceFile(.{
|
|
.file = dep.path("third_party/sqlite/sqlite3.c"),
|
|
.flags = sqliteCFlags(b, target),
|
|
});
|
|
exports_mod.link_libc = true;
|
|
}
|
|
exports_mod.export_symbol_names = &mobile_export_symbol_names;
|
|
|
|
const lib = b.addLibrary(.{
|
|
.linkage = .static,
|
|
.name = options.name,
|
|
.root_module = exports_mod,
|
|
// The embed C ABI (`native_sdk_app_viewport`) is exactly the
|
|
// f32-heavy SysV signature Zig 0.16.0's self-hosted x86_64 backend
|
|
// miscompiles (see useLlvmWorkaround in the framework build.zig):
|
|
// clang-compiled hosts calling a self-hosted Debug lib receive
|
|
// corrupted inset/keyboard floats on x86_64 (Android emulators,
|
|
// Intel simulators). Force LLVM there; Release already uses it.
|
|
.use_llvm = useLlvmWorkaround(target),
|
|
});
|
|
|
|
const lib_step = b.step("lib", "Build the mobile embed static library");
|
|
if (options.ts_core != null and target.result.abi.isAndroid()) {
|
|
// Zig's ELF static-library emission stores the compiled TypeScript
|
|
// archives as nested members instead of merging their objects (the
|
|
// Mach-O emission merges), and the NDK's -shared host link would
|
|
// skip those blobs with only a warning. Flatten to one plain
|
|
// object archive so the Android host tier keeps linking exactly
|
|
// the archive it already stages.
|
|
const merged = mergeMobileArchive(b, dep, lib, options.name);
|
|
const lib_name = b.fmt("lib{s}.a", .{options.name});
|
|
b.getInstallStep().dependOn(&b.addInstallFileWithDir(merged, .lib, lib_name).step);
|
|
lib_step.dependOn(&b.addInstallFileWithDir(merged, .lib, lib_name).step);
|
|
} else {
|
|
b.installArtifact(lib);
|
|
lib_step.dependOn(&b.addInstallArtifact(lib, .{}).step);
|
|
}
|
|
}
|
|
|
|
/// Flatten an Android embed library whose members include the compiled
|
|
/// TypeScript archives (see the call site above). Runs under node like the
|
|
/// rest of the TypeScript lane's drivers.
|
|
fn mergeMobileArchive(b: *std.Build, dep: *std.Build.Dependency, lib: *std.Build.Step.Compile, name: []const u8) std.Build.LazyPath {
|
|
const node = b.findProgram(&.{"node"}, &.{}) catch
|
|
@panic("\nmerging the mobile TypeScript archives needs node on PATH (the TypeScript core lane already requires it).\n");
|
|
const merge = b.addSystemCommand(&.{node});
|
|
merge.addFileArg(dep.path("packages/core/scripts/merge_static_archives.mjs"));
|
|
merge.addArgs(&.{ "--zig", b.graph.zig_exe, "--format", "gnu" });
|
|
merge.addArg("--out");
|
|
const merged = merge.addOutputFileArg(b.fmt("lib{s}.a", .{name}));
|
|
merge.addArg("--in");
|
|
merge.addFileArg(lib.getEmittedBin());
|
|
return merged;
|
|
}
|
|
|
|
/// The pieces `addApp` wires, for callers that extend the standard app
|
|
/// build (extra native sources, frameworks, post-build steps such as
|
|
/// entitlement signing). `install` is the artifact-install step behind the
|
|
/// default `zig build`; append dependencies to it and to `run` to order
|
|
/// work between the emitted binary and its consumers.
|
|
pub const AppArtifacts = struct {
|
|
exe: *std.Build.Step.Compile,
|
|
tests: *std.Build.Step.Compile,
|
|
install: *std.Build.Step.InstallArtifact,
|
|
run: *std.Build.Step.Run,
|
|
};
|
|
|
|
pub fn addApp(b: *std.Build, dep: *std.Build.Dependency, app_options: AppOptions) void {
|
|
_ = addAppArtifacts(b, dep, app_options);
|
|
}
|
|
|
|
pub fn addAppArtifacts(b: *std.Build, dep: *std.Build.Dependency, app_options: AppOptions) AppArtifacts {
|
|
const target = nativeSdkTarget(b);
|
|
const optimize_request = b.option(std.builtin.OptimizeMode, "optimize", "Prioritize performance, safety, or binary size");
|
|
const optimize = exampleOptimizeMode(b, optimize_request, .Debug);
|
|
const app_optimize = exampleOptimizeMode(b, optimize_request, .ReleaseFast);
|
|
const build_trace = b.option(bool, "build-trace", "Trace generated TypeScript ABI artifacts and cache reuse") orelse false;
|
|
const scriptc_optimization = scriptcProfileOptimization(b, dep, app_optimize);
|
|
|
|
// The core role is detected from the tree (never a flag or config):
|
|
// builds with a custom `main` entry declared their core explicitly and
|
|
// skip detection.
|
|
const core_tree: CoreTree = if (std.mem.eql(u8, app_options.main, "src/main.zig"))
|
|
detectCoreTree(b, app_options.app_root)
|
|
else
|
|
.zig;
|
|
if (core_tree == .both) {
|
|
@panic("\nthis app declares two cores: src/core.ts (TypeScript) and src/main.zig (Zig)." ++
|
|
"\nAn app has exactly one core - the tree is the truth. Keep src/core.ts and delete" ++
|
|
" src/main.zig,\nor keep src/main.zig and delete src/core.ts. (Other Zig files under" ++
|
|
" src/ are fine either way.)\n");
|
|
}
|
|
const manifest_name = appManifestName(b, app_options.app_root, app_options.manifest);
|
|
const app_config = appManifestBuildConfig(b, app_options.app_root, manifest_name);
|
|
// The core-compiler setting names the one lane there is; the flag
|
|
// overrides app.zon's `.core_compiler` and both exist so a stated
|
|
// choice stays stateable (and so the removed lane's spelling teaches
|
|
// instead of failing opaquely).
|
|
if (b.option([]const u8, "core-compiler", "How a TypeScript core compiles: external (the default and only lane)")) |flag| {
|
|
if (std.mem.eql(u8, flag, "transpiler")) @panic(core_compiler_teaching);
|
|
if (!std.mem.eql(u8, flag, "external")) @panic("\n-Dcore-compiler must be \"external\" (the default and only lane)\n");
|
|
if (core_tree != .ts) {
|
|
@panic("\n-Dcore-compiler applies to TypeScript cores (src/core.ts) only; this app has" ++
|
|
" a Zig core.\nDrop the flag or port the core to TypeScript.\n");
|
|
}
|
|
}
|
|
// The service-carrier selection: `-Dservice-carrier` overrides app.zon's
|
|
// `.service_carrier`; both default to auto (the child carrier).
|
|
const service_carrier_choice: ServiceCarrierOption = choice: {
|
|
if (b.option([]const u8, "service-carrier", "How src/services operations run: auto (default), in_process, child")) |flag| {
|
|
break :choice parseServiceCarrierOption(flag);
|
|
}
|
|
break :choice parseServiceCarrierOption(app_config.service_carrier);
|
|
};
|
|
const service_pool_workers: ?u8 = workers: {
|
|
const configured = b.option(u8, "service-pool-size", "In-process service carrier worker threads (1-16; default min(4, cores))") orelse
|
|
app_config.service_pool_size;
|
|
if (configured) |count| {
|
|
if (count < 1 or count > 16) @panic("\nservice pool size must be between 1 and 16\n");
|
|
}
|
|
break :workers configured;
|
|
};
|
|
const ts_stage: ?TsCoreStage = if (core_tree == .ts)
|
|
tsCoreStage(
|
|
b,
|
|
dep,
|
|
target,
|
|
app_options.app_root,
|
|
app_options.name,
|
|
app_config.persist_capability,
|
|
app_config.store_capability,
|
|
app_config.relational_capability,
|
|
app_config.credentials_capability,
|
|
app_config.credentials_permission,
|
|
app_config.persist_version,
|
|
app_config.service_packages,
|
|
service_carrier_choice,
|
|
service_pool_workers,
|
|
build_trace,
|
|
scriptc_optimization,
|
|
)
|
|
else
|
|
null;
|
|
const relational_migrations = if (ts_stage) |stage|
|
|
stage.migrations
|
|
else if (app_config.relational_capability)
|
|
sqliteMigrationsStage(b, dep, app_options.app_root)
|
|
else
|
|
dep.path("src/app_runner/no_migrations.zig");
|
|
|
|
// Mobile targets get the embed static library as a `lib` step: the
|
|
// artifact the toolkit-owned iOS host (and any hand-written shim)
|
|
// links, so `native dev|package --target ios` works against every
|
|
// standard app build — generated graph or ejected — with nothing but
|
|
// `-Dtarget`. Desktop targets keep the step absent. A TypeScript core
|
|
// roots the library's app module at the staged mobile wiring and
|
|
// merges the compiled archives into it.
|
|
if (target.result.os.tag == .ios or target.result.abi.isAndroid()) {
|
|
addMobileLibWithTarget(b, dep, target, optimize, .{
|
|
.name = app_options.name,
|
|
.main = appPath(b, app_options.app_root, app_options.main),
|
|
.store_capability = app_config.store_capability,
|
|
.relational_capability = app_config.relational_capability,
|
|
.relational_migrations = relational_migrations,
|
|
.credentials_capability = app_config.credentials_capability,
|
|
.credentials_permission = app_config.credentials_permission,
|
|
.filesystem_permission = app_config.filesystem_permission,
|
|
.credentials_service = app_config.app_id,
|
|
.max_image_pixel_bytes = app_config.max_image_pixel_bytes,
|
|
.ts_core = if (ts_stage) |stage| .{
|
|
.main_root = stage.mobile_root,
|
|
.archive = stage.archive,
|
|
.service_archive = stage.service_archive,
|
|
.markup_c = stage.markup_c,
|
|
.manifest_mod = appManifestModule(b, app_options.app_root, manifest_name),
|
|
} else null,
|
|
});
|
|
}
|
|
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 web_layer_override = b.option(WebLayerOption, "web-layer", "Override app.zon webview_layer: auto, include, exclude");
|
|
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 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 web_engine = web_engine_override orelse app_config.web_engine;
|
|
if (app_config.updates_enabled and selected_platform == .macos and web_engine == .chromium) {
|
|
@panic("\nnative updates currently require the system macOS host; use web_engine = \"system\" or remove the updates block\n");
|
|
}
|
|
const cef_dir = cef_dir_override orelse defaultCefDir(selected_platform, app_config.cef_dir);
|
|
const cef_auto_install = cef_auto_install_override orelse app_config.cef_auto_install;
|
|
if (web_engine == .chromium and selected_platform != .macos) {
|
|
@panic("-Dweb-engine=chromium currently requires -Dplatform=macos");
|
|
}
|
|
const web_layer = resolveWebLayer(app_config, web_engine, web_layer_override);
|
|
|
|
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);
|
|
options.addOption(bool, "web_layer", web_layer);
|
|
const options_mod = options.createModule();
|
|
|
|
const app_mod = appModule(b, dep, target, app_optimize, app_options, manifest_name, options_mod, ts_stage, relational_migrations, app_config);
|
|
// TypeScript app code and platform hosts are expensive Zig/Clang semantic
|
|
// work but do not depend on primary markup bytes. Compile them once into
|
|
// an object, then make the executable a link-only artifact over that
|
|
// cached object plus the tiny generated markup-data object. A markup edit
|
|
// therefore avoids re-analyzing the SDK and app runner entirely.
|
|
const exe_root = if (ts_stage) |stage| root: {
|
|
const app_code = b.addObject(.{
|
|
.name = b.fmt("{s}-app-code", .{app_options.name}),
|
|
.root_module = app_mod,
|
|
.use_llvm = useLlvmWorkaround(target),
|
|
});
|
|
const link_mod = b.createModule(.{ .target = target, .optimize = app_optimize });
|
|
link_mod.addObject(app_code);
|
|
link_mod.addObject(markupDataObject(b, target, app_optimize, stage.markup_c));
|
|
// Object dependencies propagate framework/system-library NAMES, but
|
|
// Zig does not propagate the search paths or rpaths recorded on the
|
|
// module that produced an object. Restate those path-only facts on
|
|
// the final link module so the cached app-code split remains link-
|
|
// equivalent to compiling the app module directly.
|
|
addPlatformLinkSearchPaths(b, selected_platform, web_engine, cef_dir, link_mod);
|
|
break :root link_mod;
|
|
} else app_mod;
|
|
const exe = b.addExecutable(.{
|
|
.name = app_options.name,
|
|
.root_module = exe_root,
|
|
// The app executable crosses the platform C seam on every host
|
|
// call (the GTK host's `native_sdk_gtk_create_view` is a
|
|
// 22-parameter mix of pointers, sizes, ints, and doubles), and
|
|
// Zig 0.16.0's self-hosted x86_64 backend miscompiles exactly
|
|
// that calling-convention shape (see useLlvmWorkaround below):
|
|
// a Debug `native dev` on x86_64 Linux placed the stack-passed
|
|
// string arguments one slot off, so the host read a garbage
|
|
// `role` pointer and crashed in `native_sdk_strndup` while the
|
|
// register-passed `label` arrived intact. Every other artifact
|
|
// in this graph already forces LLVM on x86_64; the app exe —
|
|
// the one binary users actually run — must too.
|
|
.use_llvm = useLlvmWorkaround(target),
|
|
});
|
|
// Windows subsystem posture: release-shaped exes (`native build`,
|
|
// and therefore everything `native package --target windows` wraps)
|
|
// are GUI-subsystem, so launching the app never flashes a console
|
|
// window behind it. Debug exes keep the console subsystem — the dev
|
|
// loop's logs live there, and a double-clicked Debug binary opening
|
|
// its own log console is a feature. Redirected logging still works
|
|
// on GUI exes (handles inherit; only console AUTO-allocation is
|
|
// gated by the subsystem), so automation harnesses that pipe
|
|
// `app.exe > log 2>&1` keep their logs either way.
|
|
if (target.result.os.tag == .windows and app_optimize != .Debug) {
|
|
exe.subsystem = .windows;
|
|
}
|
|
linkPlatform(b, dep, target, app_mod, exe, selected_platform, web_engine, web_layer, cef_dir, cef_auto_install);
|
|
const install = b.addInstallArtifact(exe, .{});
|
|
b.getInstallStep().dependOn(&install.step);
|
|
|
|
const service_install: ?*std.Build.Step.InstallFile = if (ts_stage) |stage| if (stage.service_exe) |service_exe| service_install: {
|
|
const suffix = if (target.result.os.tag == .windows) ".exe" else "";
|
|
const value = b.addInstallFileWithDir(service_exe, .bin, b.fmt("{s}_services{s}", .{ app_options.name, suffix }));
|
|
b.getInstallStep().dependOn(&value.step);
|
|
break :service_install value;
|
|
} else null else null;
|
|
|
|
// A service-bearing app runs from the installed layout so the host is
|
|
// a real sibling exactly as it is in a package. Service-free apps keep
|
|
// the direct cached-artifact fast path.
|
|
const run = if (service_install != null) run: {
|
|
const suffix = if (target.result.os.tag == .windows) ".exe" else "";
|
|
const value = b.addSystemCommand(&.{b.getInstallPath(.bin, b.fmt("{s}{s}", .{ app_options.name, suffix }))});
|
|
value.step.dependOn(&install.step);
|
|
value.step.dependOn(&service_install.?.step);
|
|
break :run value;
|
|
} else b.addRunArtifact(exe);
|
|
addCefRuntimeRunFiles(b, target, run, exe, web_engine, cef_dir);
|
|
addWebView2RuntimeRunFiles(dep, target, run, web_engine, web_layer);
|
|
const run_step = b.step("run", "Run the app");
|
|
run_step.dependOn(&run.step);
|
|
|
|
// TypeScript tests need their own module even when optimize modes match:
|
|
// the production app module feeds the cached app-code object and must not
|
|
// absorb the separately linked markup data object.
|
|
const test_app_mod = if (ts_stage != null or app_optimize != optimize)
|
|
appModule(b, dep, target, optimize, app_options, manifest_name, options_mod, ts_stage, relational_migrations, app_config)
|
|
else
|
|
app_mod;
|
|
if (ts_stage) |stage| test_app_mod.addObject(markupDataObject(b, target, optimize, stage.markup_c));
|
|
const tests = b.addTest(.{ .root_module = test_app_mod, .use_llvm = useLlvmWorkaround(target) });
|
|
const test_step = b.step("test", "Run tests");
|
|
test_step.dependOn(&b.addRunArtifact(tests).step);
|
|
|
|
// `native test` must surface the app's compile-time teaching errors,
|
|
// not just its test failures. Test builds never analyze `main` (the
|
|
// test runner replaces the entry point), so rules that fire inside it
|
|
// — UiApp.create's Model-defaults rule above all — used to ambush at
|
|
// `native build`, the LAST step in the loop. Compiling this object
|
|
// forces full semantic analysis of the app module, entry point
|
|
// included; nothing links or runs.
|
|
const analysis_root = b.addWriteFiles().add("app_analysis.zig",
|
|
\\//! Generated by the app build: force semantic analysis of the
|
|
\\//! app's entry point at test time. Exactly `main`, transitively —
|
|
\\//! the same surface `native build` analyzes — so test-time can
|
|
\\//! never be stricter than the build it fronts.
|
|
\\comptime {
|
|
\\ const app = @import("app");
|
|
\\ if (@hasDecl(app, "main")) _ = &app.main;
|
|
\\}
|
|
\\
|
|
);
|
|
const analysis_mod = b.createModule(.{
|
|
.root_source_file = analysis_root,
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
analysis_mod.addImport("app", test_app_mod);
|
|
const analysis_obj = b.addObject(.{
|
|
.name = b.fmt("{s}-analysis", .{app_options.name}),
|
|
.root_module = analysis_mod,
|
|
.use_llvm = useLlvmWorkaround(target),
|
|
});
|
|
test_step.dependOn(&analysis_obj.step);
|
|
|
|
// `zig build model-contract`: reflect the app's Model/Msg into
|
|
// zig-out/model-contract.zon so `native check` can verify markup
|
|
// bindings against the app's real surface without compiling the app.
|
|
// The artifact carries a hash over the app's Zig sources; the checker
|
|
// degrades to structural checking when it goes stale. Apps without a
|
|
// pub Model/Msg pair make this a silent no-op. The test step refreshes
|
|
// the artifact too, so CI-checked apps always hold a fresh one.
|
|
const contract_root = b.addWriteFiles().add("model_contract_emit.zig",
|
|
\\//! Generated by the app build: emits the model contract artifact
|
|
\\//! (see the toolkit's ui_markup_contract.zig).
|
|
\\const std = @import("std");
|
|
\\const native_sdk = @import("native_sdk");
|
|
\\const app = @import("app");
|
|
\\
|
|
\\pub fn main(init: std.process.Init) !void {
|
|
\\ try native_sdk.canvas.emitModelContractMain(app, init);
|
|
\\}
|
|
\\
|
|
);
|
|
// The emit root must share the app module's native_sdk instance so
|
|
// the Msg payload types it classifies are the same types the app
|
|
// declares its variants with.
|
|
const contract_mod = b.createModule(.{
|
|
.root_source_file = contract_root,
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
contract_mod.addImport("app", test_app_mod);
|
|
if (test_app_mod.import_table.get("native_sdk")) |sdk_mod| {
|
|
contract_mod.addImport("native_sdk", sdk_mod);
|
|
}
|
|
const contract_exe = b.addExecutable(.{
|
|
.name = b.fmt("{s}-model-contract", .{app_options.name}),
|
|
.root_module = contract_mod,
|
|
.use_llvm = useLlvmWorkaround(target),
|
|
});
|
|
const contract_run = b.addRunArtifact(contract_exe);
|
|
contract_run.setCwd(b.path(app_options.app_root));
|
|
contract_run.addArgs(&.{ "--src", "src", "--out", "zig-out/model-contract.zon" });
|
|
contract_run.has_side_effects = true;
|
|
const contract_step = b.step("model-contract", "Emit zig-out/model-contract.zon for `native check`");
|
|
contract_step.dependOn(&contract_run.step);
|
|
test_step.dependOn(&contract_run.step);
|
|
|
|
// `zig build package`: bundle the built binary through the `native`
|
|
// CLI (built from the native_sdk dependency), so a scaffolded app can
|
|
// package itself without locating the CLI by hand.
|
|
const host_os = b.graph.host.result.os.tag;
|
|
const package_target: ?[]const u8 = switch (host_os) {
|
|
.macos => "macos",
|
|
.linux => "linux",
|
|
.windows => "windows",
|
|
else => null,
|
|
};
|
|
if (package_target) |package_target_name| {
|
|
const package_run = b.addRunArtifact(dep.artifact("native"));
|
|
// The CLI resolves SDK-owned package inputs (the vendored
|
|
// WebView2 loader) from the framework root; the cached artifact's
|
|
// own location cannot derive it, so hand it over explicitly.
|
|
package_run.setEnvironmentVariable("NATIVE_SDK_PATH", dep.builder.pathFromRoot("."));
|
|
package_run.addArgs(&.{ "package", "--target", package_target_name, "--manifest", manifest_name, "--output" });
|
|
package_run.addArg(if (host_os == .macos)
|
|
b.fmt("zig-out/package/{s}.app", .{app_options.name})
|
|
else
|
|
b.fmt("zig-out/package/{s}", .{package_target_name}));
|
|
package_run.addArg("--binary");
|
|
package_run.addFileArg(exe.getEmittedBin());
|
|
if (ts_stage) |stage| if (stage.service_exe) |service_exe| {
|
|
package_run.addArg("--service-binary");
|
|
package_run.addFileArg(service_exe);
|
|
};
|
|
// The archive and report names carry an optimize label; this
|
|
// build graph knows the packaged binary's REAL mode, so forward
|
|
// it instead of letting the CLI assume one.
|
|
package_run.addArgs(&.{ "--optimize", @tagName(app_optimize) });
|
|
// Forward the RESOLVED web-layer decision, never the raw inputs:
|
|
// this graph already decided web vs native-only for the exe it is
|
|
// packaging (app.zon declarations plus -Dweb-layer/-Dweb-engine),
|
|
// and the CLI re-inferring from app.zon alone would miss a
|
|
// flag-driven override — a WebView2-referencing exe packaged
|
|
// without its loader. Handing over the decision itself makes
|
|
// exe/package agreement structural.
|
|
package_run.addArgs(&.{ "--web-layer", if (web_layer) "include" else "exclude" });
|
|
// Same reasoning for the web engine: the CLI defaults to system, so
|
|
// a Chromium exe packaged without these flags would ship no CEF
|
|
// runtime (the generated build graph already forwards them).
|
|
package_run.addArgs(&.{ "--web-engine", @tagName(web_engine), "--cef-dir", cef_dir });
|
|
if (cef_auto_install) package_run.addArg("--cef-auto-install");
|
|
if (app_config.updates_enabled and host_os == .macos) package_run.addArg("--update-archive");
|
|
package_run.has_side_effects = true;
|
|
const package_step = b.step("package", "Create a distributable package via the native CLI");
|
|
package_step.dependOn(&package_run.step);
|
|
}
|
|
|
|
return .{ .exe = exe, .tests = tests, .install = install, .run = run };
|
|
}
|
|
|
|
/// Zig 0.16.0's self-hosted x86_64 backend miscompiles the SysV C calling
|
|
/// convention for f32-heavy signatures with interleaved pointer arguments
|
|
/// (`native_sdk_app_viewport`: 11 f32s + 2 pointers): both the caller and
|
|
/// the callee place/read the wrong registers and stack slots, so safe-area
|
|
/// insets arrive as garbage on x86_64 Debug builds while every LLVM-backed
|
|
/// build is correct. Minimal repro (fails under `zig test`, passes with
|
|
/// `-fllvm` on x86_64-linux):
|
|
///
|
|
/// fn take(a: ?*anyopaque, w: f32, h: f32, s: f32, p: ?*anyopaque,
|
|
/// t: f32, r: f32, bo: f32, l: f32, kt: f32, kr: f32, kb: f32,
|
|
/// kl: f32) callconv(.c) void { ... }
|
|
///
|
|
/// The same backend also mis-places STACK-passed integer/pointer arguments
|
|
/// in long mixed signatures with interleaved doubles: calling the GTK
|
|
/// host's `native_sdk_gtk_create_view` (22 params: 6 register ints, 4
|
|
/// doubles, 12 stack ints/pointers/sizes) from self-hosted Debug code
|
|
/// hands the clang-compiled callee arguments shifted by one stack slot
|
|
/// from `visible` onward — the callee's `role` pointer reads as the
|
|
/// caller's `enabled` value (a 4-byte 1 under 0xAA undefined fill,
|
|
/// faulting at 0xaaaaaaaa00000001) and `role_len` reads as the role
|
|
/// pointer. Register-passed arguments (`label`) arrive intact, which is
|
|
/// why the crash appears only at the first stack-passed string. Verified
|
|
/// against zig 0.16.0 on x86_64-linux with a standalone caller/callee
|
|
/// pair: self-hosted Debug corrupts, `-fllvm` is correct.
|
|
///
|
|
/// Force the LLVM backend on x86_64 until the upstream backend is fixed;
|
|
/// Release modes already default to LLVM, so this only changes Debug.
|
|
pub fn useLlvmWorkaround(target: std.Build.ResolvedTarget) ?bool {
|
|
return if (target.result.cpu.arch == .x86_64) true else null;
|
|
}
|
|
|
|
fn exampleOptimizeMode(b: *std.Build, requested: ?std.builtin.OptimizeMode, default_mode: std.builtin.OptimizeMode) std.builtin.OptimizeMode {
|
|
if (requested) |mode| return mode;
|
|
return switch (b.release_mode) {
|
|
.off => default_mode,
|
|
.any, .fast => .ReleaseFast,
|
|
.safe => .ReleaseSafe,
|
|
.small => .ReleaseSmall,
|
|
};
|
|
}
|
|
|
|
fn appModule(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, app_options: AppOptions, manifest_name: []const u8, options_mod: *std.Build.Module, ts_stage: ?TsCoreStage, relational_migrations: std.Build.LazyPath, app_config: AppManifestBuildConfig) *std.Build.Module {
|
|
const native_sdk_mod = nativeSdkModuleWithTerminal(b, dep, target, optimize, app_options.terminal_sessions);
|
|
const runner_mod = b.createModule(.{
|
|
.root_source_file = dep.path("src/app_runner/root.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
const manifest_mod = appManifestModule(b, app_options.app_root, manifest_name);
|
|
runner_mod.addImport("native_sdk", native_sdk_mod);
|
|
runner_mod.addImport("build_options", options_mod);
|
|
runner_mod.addImport("app_manifest_zon", manifest_mod);
|
|
const migration_mod = b.createModule(.{ .root_source_file = relational_migrations, .target = target, .optimize = optimize });
|
|
migration_mod.addImport("native_sdk", native_sdk_mod);
|
|
runner_mod.addImport("relational_migrations", migration_mod);
|
|
|
|
const app_mod = if (ts_stage) |stage|
|
|
// TypeScript core: the app module roots at the staged generated
|
|
// wiring (ts_core_main.zig beside the mirror core.zig, its shim
|
|
// runtime; primary markup bytes link as a separate data object).
|
|
b.createModule(.{
|
|
.root_source_file = stage.main_root,
|
|
.target = target,
|
|
.optimize = optimize,
|
|
})
|
|
else
|
|
localModule(b, target, optimize, appPath(b, app_options.app_root, app_options.main));
|
|
app_mod.addImport("native_sdk", native_sdk_mod);
|
|
app_mod.addImport("runner", runner_mod);
|
|
if (ts_stage != null) {
|
|
// The wiring derives scene/identity/security from app.zon itself.
|
|
app_mod.addImport("app_manifest_zon", manifest_mod);
|
|
}
|
|
if (ts_stage) |stage| {
|
|
// The compiled-core archive links behind the staged mirror; the
|
|
// toolchain's runtime needs libc.
|
|
app_mod.link_libc = true;
|
|
app_mod.addObjectFile(stage.archive);
|
|
// The in-process service archive links beside it (distinct symbol
|
|
// prefix; its runtime internals are localized).
|
|
if (stage.service_archive) |service_archive| app_mod.addObjectFile(service_archive);
|
|
}
|
|
if (app_config.sqlite_capability) {
|
|
// `store` and the relational `sqlite` tier share this exact object.
|
|
// The source is absent from every artifact declaring neither
|
|
// capability, which keeps capability inference a real binary-size
|
|
// boundary rather than a runtime flag.
|
|
app_mod.addIncludePath(dep.path("third_party/sqlite"));
|
|
app_mod.addCSourceFile(.{
|
|
.file = dep.path("third_party/sqlite/sqlite3.c"),
|
|
.flags = &sqlite_c_defines,
|
|
});
|
|
app_mod.link_libc = true;
|
|
}
|
|
addMacosInfoPlist(b, app_mod, target, app_config);
|
|
return app_mod;
|
|
}
|
|
|
|
/// Compile primary markup bytes independently from the Zig app graph. The
|
|
/// final artifact sees this as an object-file input, so a markup edit dirties
|
|
/// only this tiny C compile and the final link.
|
|
fn markupDataObject(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, source: std.Build.LazyPath) *std.Build.Step.Compile {
|
|
const mod = b.createModule(.{ .target = target, .optimize = optimize });
|
|
mod.addCSourceFile(.{ .file = source, .flags = &.{} });
|
|
return b.addObject(.{ .name = "native-app-markup-data", .root_module = mod });
|
|
}
|
|
|
|
/// Bare Mach-O executables launched by the dev loop do not have an app
|
|
/// bundle's external Info.plist. Embed launch policy plus capture usage
|
|
/// strings in the conventional section so LaunchServices starts accessory
|
|
/// apps without a transient Dock tile and capture consent can be presented.
|
|
/// Packaged apps retain their richer external plist generated by package.zig.
|
|
fn addMacosInfoPlist(b: *std.Build, app_mod: *std.Build.Module, target: std.Build.ResolvedTarget, config: AppManifestBuildConfig) void {
|
|
if (target.result.os.tag != .macos) return;
|
|
if (config.dock_visible and !config.microphone_permission and !config.system_audio_permission) return;
|
|
|
|
const launch_policy = if (!config.dock_visible)
|
|
" <key>LSUIElement</key>\\n <true/>\\n"
|
|
else
|
|
"";
|
|
const microphone = if (config.microphone_permission)
|
|
" <key>NSMicrophoneUsageDescription</key>\\n <string>This app captures microphone audio when you start recording.</string>\\n"
|
|
else
|
|
"";
|
|
const system_audio = if (config.system_audio_permission)
|
|
" <key>NSAudioCaptureUsageDescription</key>\\n <string>This app captures system audio when you start recording.</string>\\n" ++
|
|
" <key>NSScreenCaptureUsageDescription</key>\\n <string>This app captures system audio when you start recording.</string>\\n"
|
|
else
|
|
"";
|
|
const source = b.fmt(
|
|
\\#define NATIVE_SDK_INFO_PLIST "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n" "<plist version=\"1.0\">\n<dict>\n{s}{s}{s}</dict>\n</plist>\n"
|
|
\\__attribute__((used, section("__TEXT,__info_plist")))
|
|
\\static const unsigned char native_sdk_info_plist[sizeof(NATIVE_SDK_INFO_PLIST) - 1] = NATIVE_SDK_INFO_PLIST;
|
|
\\
|
|
, .{ launch_policy, microphone, system_audio });
|
|
const generated = b.addWriteFiles().add("native_sdk_macos_info_plist.c", source);
|
|
app_mod.addCSourceFile(.{ .file = generated, .flags = &.{} });
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
const sqlite_c_defines = [_][]const u8{
|
|
"-DSQLITE_THREADSAFE=2",
|
|
"-DSQLITE_OMIT_LOAD_EXTENSION",
|
|
"-DSQLITE_DQS=0",
|
|
"-DSQLITE_ENABLE_FTS5",
|
|
"-DSQLITE_ENABLE_JSON1",
|
|
"-DSQLITE_ENABLE_UPDATE_HOOK",
|
|
"-DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1",
|
|
"-DSQLITE_DEFAULT_MEMSTATUS=0",
|
|
};
|
|
|
|
/// Zig deliberately supplies no libc headers for Apple/Android cross targets.
|
|
/// Store-capable mobile libraries therefore compile the vendored amalgamation
|
|
/// against the same platform SDK the host tier will use to link the archive.
|
|
/// Desktop targets keep Zig's ordinary libc discovery. Pub because the SDK's
|
|
/// own build graph compiles the same amalgamation into modules that also
|
|
/// configure under a mobile -Dtarget (the mobile e2e battery).
|
|
pub fn sqliteCFlags(b: *std.Build, target: std.Build.ResolvedTarget) []const []const u8 {
|
|
if (target.result.os.tag == .ios) {
|
|
const sysroot = b.sysroot orelse iosSdkPath(b, target.result.abi == .simulator) orelse
|
|
std.debug.panic("a store-capable iOS library needs the Apple SDK; install Xcode or pass --sysroot <iphone SDK path>", .{});
|
|
return b.dupeStrings(&.{
|
|
sqlite_c_defines[0],
|
|
sqlite_c_defines[1],
|
|
sqlite_c_defines[2],
|
|
sqlite_c_defines[3],
|
|
sqlite_c_defines[4],
|
|
sqlite_c_defines[5],
|
|
sqlite_c_defines[6],
|
|
sqlite_c_defines[7],
|
|
"-isysroot",
|
|
sysroot,
|
|
b.fmt("-isystem{s}/usr/include", .{sysroot}),
|
|
});
|
|
}
|
|
if (target.result.abi.isAndroid()) {
|
|
const sysroot = b.sysroot orelse androidNdkSysrootPath(b) orelse
|
|
std.debug.panic("a store-capable Android library needs the NDK; set ANDROID_NDK_ROOT or ANDROID_HOME, or pass --sysroot <NDK sysroot>", .{});
|
|
const triple = target.result.linuxTriple(b.allocator) catch @panic("out of memory");
|
|
return b.dupeStrings(&.{
|
|
sqlite_c_defines[0],
|
|
sqlite_c_defines[1],
|
|
sqlite_c_defines[2],
|
|
sqlite_c_defines[3],
|
|
sqlite_c_defines[4],
|
|
sqlite_c_defines[5],
|
|
sqlite_c_defines[6],
|
|
sqlite_c_defines[7],
|
|
b.fmt("-isystem{s}/usr/include/{s}", .{ sysroot, triple }),
|
|
b.fmt("-isystem{s}/usr/include", .{sysroot}),
|
|
});
|
|
}
|
|
return &sqlite_c_defines;
|
|
}
|
|
|
|
fn iosSdkPath(b: *std.Build, simulator: bool) ?[]const u8 {
|
|
const result = std.process.run(b.allocator, b.graph.io, .{
|
|
.argv = &.{ "xcrun", "--sdk", if (simulator) "iphonesimulator" else "iphoneos", "--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");
|
|
}
|
|
|
|
/// Thread the Android NDK location into a ScriptC driver invocation the
|
|
/// way `--zig-exe` threads this build's zig: resolved here, at the one
|
|
/// boundary that already knows how to discover it, so the compiler's own
|
|
/// discovery never depends on the ambient environment. A missing NDK stays
|
|
/// quiet — the driver and compiler own the teaching when an Android
|
|
/// compile actually needs one.
|
|
pub fn addScriptcAndroidNdk(b: *std.Build, run: *std.Build.Step.Run, target: std.Build.ResolvedTarget) void {
|
|
if (!target.result.abi.isAndroid()) return;
|
|
const ndk_root = androidNdkRootPath(b) orelse return;
|
|
run.addArgs(&.{ "--android-ndk", ndk_root });
|
|
}
|
|
|
|
/// The NDK's root directory (the directory holding toolchains/llvm):
|
|
/// ANDROID_NDK_ROOT/ANDROID_NDK_HOME wins, else the newest ndk/<version>
|
|
/// under the platform SDK location — the same order the compiler's own
|
|
/// discovery uses, so threading it changes nothing but the authority.
|
|
fn androidNdkRootPath(b: *std.Build) ?[]const u8 {
|
|
for ([_][]const u8{ "ANDROID_NDK_ROOT", "ANDROID_NDK_HOME", "ANDROID_NDK_LATEST_HOME" }) |name| {
|
|
if (b.graph.environ_map.get(name)) |root| {
|
|
if (root.len > 0 and buildDirExists(b, root)) return root;
|
|
}
|
|
}
|
|
const sdk_root = androidSdkRoot(b) orelse return null;
|
|
return latestVersionSubdir(b, sdk_root, "ndk") orelse blk: {
|
|
const legacy = b.pathJoin(&.{ sdk_root, "ndk-bundle" });
|
|
break :blk if (buildDirExists(b, legacy)) legacy else null;
|
|
};
|
|
}
|
|
|
|
fn androidNdkSysrootPath(b: *std.Build) ?[]const u8 {
|
|
for ([_][]const u8{ "ANDROID_NDK_ROOT", "ANDROID_NDK_HOME", "ANDROID_NDK_LATEST_HOME" }) |name| {
|
|
if (b.graph.environ_map.get(name)) |root| {
|
|
if (root.len > 0) {
|
|
if (ndkSysrootUnder(b, root)) |sysroot| return sysroot;
|
|
}
|
|
}
|
|
}
|
|
|
|
const sdk_root = androidSdkRoot(b) orelse return null;
|
|
const ndk_root = latestVersionSubdir(b, sdk_root, "ndk") orelse blk: {
|
|
const legacy = b.pathJoin(&.{ sdk_root, "ndk-bundle" });
|
|
break :blk if (buildDirExists(b, legacy)) legacy else return null;
|
|
};
|
|
return ndkSysrootUnder(b, ndk_root);
|
|
}
|
|
|
|
fn androidSdkRoot(b: *std.Build) ?[]const u8 {
|
|
for ([_][]const u8{ "ANDROID_HOME", "ANDROID_SDK_ROOT" }) |name| {
|
|
if (b.graph.environ_map.get(name)) |root| {
|
|
if (root.len > 0 and buildDirExists(b, root)) return root;
|
|
}
|
|
}
|
|
return switch (builtin.os.tag) {
|
|
.macos => if (b.graph.environ_map.get("HOME")) |home|
|
|
b.pathJoin(&.{ home, "Library", "Android", "sdk" })
|
|
else
|
|
null,
|
|
.windows => if (b.graph.environ_map.get("LOCALAPPDATA")) |local_app_data|
|
|
b.pathJoin(&.{ local_app_data, "Android", "Sdk" })
|
|
else
|
|
null,
|
|
else => if (b.graph.environ_map.get("HOME")) |home|
|
|
b.pathJoin(&.{ home, "Android", "Sdk" })
|
|
else
|
|
null,
|
|
};
|
|
}
|
|
|
|
fn ndkSysrootUnder(b: *std.Build, ndk_root: []const u8) ?[]const u8 {
|
|
const prebuilt_path = b.pathJoin(&.{ ndk_root, "toolchains", "llvm", "prebuilt" });
|
|
var cwd = std.Io.Dir.cwd();
|
|
var dir = cwd.openDir(b.graph.io, prebuilt_path, .{ .iterate = true }) catch return null;
|
|
defer dir.close(b.graph.io);
|
|
var iterator = dir.iterate();
|
|
while (iterator.next(b.graph.io) catch return null) |entry| {
|
|
if (entry.kind != .directory) continue;
|
|
const sysroot = b.pathJoin(&.{ prebuilt_path, entry.name, "sysroot" });
|
|
if (buildDirExists(b, b.pathJoin(&.{ sysroot, "usr", "include" }))) return sysroot;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn latestVersionSubdir(b: *std.Build, root: []const u8, parent: []const u8) ?[]const u8 {
|
|
const parent_path = b.pathJoin(&.{ root, parent });
|
|
var cwd = std.Io.Dir.cwd();
|
|
var dir = cwd.openDir(b.graph.io, parent_path, .{ .iterate = true }) catch return null;
|
|
defer dir.close(b.graph.io);
|
|
var best: ?[]const u8 = null;
|
|
defer if (best) |name| b.allocator.free(name);
|
|
var iterator = dir.iterate();
|
|
while (iterator.next(b.graph.io) catch return null) |entry| {
|
|
if (entry.kind != .directory) continue;
|
|
if (best) |current| {
|
|
if (!versionLess(current, entry.name)) continue;
|
|
b.allocator.free(current);
|
|
best = null;
|
|
}
|
|
best = b.allocator.dupe(u8, entry.name) catch @panic("out of memory");
|
|
}
|
|
const name = best orelse return null;
|
|
return b.pathJoin(&.{ parent_path, name });
|
|
}
|
|
|
|
fn versionLess(a: []const u8, b: []const u8) bool {
|
|
var a_parts = std.mem.splitScalar(u8, a, '.');
|
|
var b_parts = std.mem.splitScalar(u8, b, '.');
|
|
while (true) {
|
|
const a_part = a_parts.next();
|
|
const b_part = b_parts.next();
|
|
if (a_part == null and b_part == null) return false;
|
|
if (a_part == null) return true;
|
|
if (b_part == null) return false;
|
|
const a_num = std.fmt.parseUnsigned(u64, a_part.?, 10) catch null;
|
|
const b_num = std.fmt.parseUnsigned(u64, b_part.?, 10) catch null;
|
|
if (a_num != null and b_num != null) {
|
|
if (a_num.? != b_num.?) return a_num.? < b_num.?;
|
|
} else switch (std.mem.order(u8, a_part.?, b_part.?)) {
|
|
.lt => return true,
|
|
.gt => return false,
|
|
.eq => {},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn buildDirExists(b: *std.Build, path: []const u8) bool {
|
|
var cwd = std.Io.Dir.cwd();
|
|
var dir = cwd.openDir(b.graph.io, path, .{}) catch return false;
|
|
dir.close(b.graph.io);
|
|
return true;
|
|
}
|
|
|
|
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 nativeSdkModule(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) *std.Build.Module {
|
|
return nativeSdkModuleWithTerminal(b, dep, target, optimize, false);
|
|
}
|
|
|
|
fn nativeSdkModuleWithTerminal(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, terminal_sessions: bool) *std.Build.Module {
|
|
const geometry_mod = externalModule(b, dep, target, optimize, "src/primitives/geometry/root.zig");
|
|
const assets_mod = externalModule(b, dep, target, optimize, "src/primitives/assets/root.zig");
|
|
const app_dirs_mod = externalModule(b, dep, target, optimize, "src/primitives/app_dirs/root.zig");
|
|
const trace_mod = externalModule(b, dep, target, optimize, "src/primitives/trace/root.zig");
|
|
const app_manifest_mod = externalModule(b, dep, target, optimize, "src/primitives/app_manifest/root.zig");
|
|
const diagnostics_mod = externalModule(b, dep, target, optimize, "src/primitives/diagnostics/root.zig");
|
|
const platform_info_mod = externalModule(b, dep, target, optimize, "src/primitives/platform_info/root.zig");
|
|
const json_mod = externalModule(b, dep, target, optimize, "src/primitives/json/root.zig");
|
|
const canvas_mod = externalModule(b, dep, target, optimize, "src/primitives/canvas/root.zig");
|
|
canvas_mod.addImport("geometry", geometry_mod);
|
|
canvas_mod.addImport("json", json_mod);
|
|
const debug_mod = externalModule(b, dep, target, optimize, "src/debug/root.zig");
|
|
debug_mod.addImport("app_dirs", app_dirs_mod);
|
|
debug_mod.addImport("trace", trace_mod);
|
|
|
|
const native_sdk_mod = externalModule(b, dep, target, optimize, "src/root.zig");
|
|
// The header makes the internal wrapper parsable even when lazy exports
|
|
// are reflected; the amalgamation itself is attached below only for an
|
|
// opted-in store/sqlite artifact.
|
|
native_sdk_mod.addIncludePath(dep.path("third_party/sqlite"));
|
|
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);
|
|
// The terminal-session emulator seam (see
|
|
// `AppOptions.terminal_sessions`): an opted-in app's OWN lazy
|
|
// ghostty pin resolves here with the app module's target/optimize;
|
|
// everything else gets the stub and never traverses ghostty's
|
|
// dependency graph. An opted-in build whose pin is still unfetched
|
|
// takes the stub for THIS configure pass — the build runner fetches
|
|
// the lazy dependency and re-runs, and the second pass wires the
|
|
// real module.
|
|
native_sdk_mod.addImport("terminal_vt", terminal_vt: {
|
|
if (terminal_sessions) {
|
|
if (b.lazyDependency("ghostty", .{
|
|
.target = target,
|
|
.optimize = optimize,
|
|
// Keep the vt module pure Zig: the SIMD paths pull
|
|
// vendored C++ dependencies the session store never uses.
|
|
.simd = false,
|
|
// Only the vt MODULE is consumed: ghostty's macOS app and
|
|
// xcframework artifacts default ON for Darwin hosts and
|
|
// their configure step resolves the iOS libc, which
|
|
// aborts on a machine with only the command-line tools.
|
|
.@"emit-xcframework" = false,
|
|
.@"emit-macos-app" = false,
|
|
})) |ghostty| {
|
|
const wrapper = externalModule(b, dep, target, optimize, "src/runtime/terminal_vt_ghostty.zig");
|
|
wrapper.addImport("ghostty-vt", ghostty.module("ghostty-vt"));
|
|
break :terminal_vt wrapper;
|
|
}
|
|
}
|
|
break :terminal_vt externalModule(b, dep, target, optimize, "src/runtime/terminal_vt_stub.zig");
|
|
});
|
|
return native_sdk_mod;
|
|
}
|
|
|
|
fn externalModule(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, path: []const u8) *std.Build.Module {
|
|
return b.createModule(.{
|
|
.root_source_file = dep.path(path),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
}
|
|
|
|
// -fno-sanitize=builtin on every ObjC compile: Zig 0.16.0's Debug UBSan
|
|
// aborts any process whose first dispatch_once runs — the macOS SDK's
|
|
// inline `_dispatch_once` ends in `__builtin_assume(*predicate == ~0l)`
|
|
// (dispatch/once.h), Zig's bundled clang instruments that builtin, and the
|
|
// check fires spuriously at startup; zig's ubsan_rt then cannot even decode
|
|
// the report ("invalid enum value" / "passing zero to clz()" panics).
|
|
// Reproduced with a 10-line `zig cc` program against both the 14.5 and
|
|
// 26.0 SDKs. Release builds never hit it (no UBSan), which is why only
|
|
// Debug-built examples (standardOptimizeOption default) crashed.
|
|
fn linkPlatform(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, app_mod: *std.Build.Module, exe: *std.Build.Step.Compile, platform: PlatformOption, web_engine: WebEngineOption, web_layer: bool, cef_dir: []const u8, cef_auto_install: bool) void {
|
|
addPlatformLinkSearchPaths(b, platform, web_engine, cef_dir, app_mod);
|
|
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 = dep.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 <string.h>/<math.h>
|
|
// 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 = dep.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.linkFramework("Chromium Embedded Framework", .{});
|
|
},
|
|
}
|
|
app_mod.linkFramework("AppKit", .{});
|
|
// The audio playback service (the AppKit host's single AVPlayer).
|
|
app_mod.linkFramework("AVFoundation", .{});
|
|
app_mod.linkFramework("CoreMedia", .{});
|
|
app_mod.linkFramework("ScreenCaptureKit", .{ .weak = true });
|
|
// CVPixelBuffer for the video frame path (the video player's
|
|
// AVPlayerItemVideoOutput frames). CoreMedia's CMTime use stays
|
|
// header-only, but the pixel-buffer calls are real symbols.
|
|
app_mod.linkFramework("CoreVideo", .{});
|
|
// 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 => if (web_layer) {
|
|
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/linux/gtk_host.c"), .flags = &.{} });
|
|
app_mod.linkSystemLibrary("gtk4", .{});
|
|
app_mod.linkSystemLibrary("webkitgtk-6.0", .{});
|
|
app_mod.linkSystemLibrary("dl", .{});
|
|
} else {
|
|
// Native-only app (nothing in app.zon declares web use):
|
|
// compile the GTK host without the embedded web layer.
|
|
// The stub define excludes the layer outright — the host
|
|
// honors it before probing for the WebKitGTK header, so
|
|
// the layer stays out even on machines where the
|
|
// development package is installed — libwebkitgtk is
|
|
// neither linked nor required at runtime, and the
|
|
// executable carries no WebKit reference at all. This
|
|
// is the expected, configured state of every canvas
|
|
// app on Linux, so the stub compile is deliberately
|
|
// silent — no build note, no compiler diagnostic (the
|
|
// host's seam comment explains why even an
|
|
// informational pragma is dangerous); a stubbed host
|
|
// teaches at runtime by reporting WebViewNotFound the
|
|
// moment an app actually uses a WebView.
|
|
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/linux/gtk_host.c"), .flags = &.{"-DNATIVE_SDK_ALLOW_WEBKITGTK_STUB"} });
|
|
app_mod.linkSystemLibrary("gtk4", .{});
|
|
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 = dep.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.linkSystemLibrary("cef", .{});
|
|
},
|
|
}
|
|
app_mod.linkSystemLibrary("c", .{});
|
|
if (web_engine == .chromium) app_mod.linkSystemLibrary("stdc++", .{});
|
|
} else if (platform == .windows) {
|
|
// Common-controls v6 side-by-side dependency: without this
|
|
// manifest the loader binds the system-default v5 assembly, which
|
|
// renders classic-styled controls and lacks the v6-only exports.
|
|
// The manifest also declares per-monitor-v2 DPI awareness so the
|
|
// canvas rasterizes at real device scale instead of Windows
|
|
// bitmap-stretching a 96-DPI surface on scaled displays.
|
|
exe.win32_manifest = dep.path("assets/native-sdk.manifest");
|
|
switch (web_engine) {
|
|
.system => if (web_layer) {
|
|
// 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(dep.path("third_party/webview2/include"));
|
|
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/webview2_host.cpp"), .flags = &.{"-std=c++17"} });
|
|
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/gpu_surface_renderer.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(dep.path(webView2LoaderSubPath(target)), "WebView2Loader.dll");
|
|
b.getInstallStep().dependOn(&loader.step);
|
|
} else {
|
|
// Native-only app (nothing in app.zon declares web use):
|
|
// compile the host without the embedded-WebView layer.
|
|
// The stub define excludes the layer outright — the host
|
|
// honors it before probing for the WebView2 header, so
|
|
// the layer stays out even on machines where the SDK
|
|
// headers are reachable through the system include paths
|
|
// — no WebView2Loader.dll is installed or path-wired,
|
|
// and the executable carries no reference to it at all.
|
|
// This is the expected, configured state of every
|
|
// canvas app on Windows, so the stub compile is
|
|
// deliberately silent — no build note, no compiler
|
|
// diagnostic (the host's seam comment explains why
|
|
// even an informational pragma is dangerous); a
|
|
// stubbed host teaches at runtime by reporting
|
|
// WebViewNotFound the moment an app actually uses a
|
|
// WebView.
|
|
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/webview2_host.cpp"), .flags = &.{ "-std=c++17", "-DNATIVE_SDK_ALLOW_WEBVIEW2_STUB" } });
|
|
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/gpu_surface_renderer.cpp"), .flags = &.{"-std=c++17"} });
|
|
},
|
|
.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 = dep.path("src/platform/windows/cef_host.cpp"), .flags = &.{ "-std=c++17", include_arg, define_arg } });
|
|
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/gpu_surface_renderer.cpp"), .flags = &.{"-std=c++17"} });
|
|
app_mod.addObjectFile(b.path(b.fmt("{s}/libcef_dll_wrapper/libcef_dll_wrapper.lib", .{cef_dir})));
|
|
},
|
|
}
|
|
app_mod.linkSystemLibrary("c", .{});
|
|
app_mod.linkSystemLibrary("c++", .{});
|
|
app_mod.linkSystemLibrary("user32", .{});
|
|
app_mod.linkSystemLibrary("gdi32", .{});
|
|
// Retained gpu_surface packets are composited into a hardware
|
|
// Direct2D target; DirectWrite draws the engine-measured text
|
|
// runs (including registered in-memory fonts) on that target.
|
|
app_mod.linkSystemLibrary("d2d1", .{});
|
|
app_mod.linkSystemLibrary("dwrite", .{});
|
|
app_mod.linkSystemLibrary("imm32", .{});
|
|
app_mod.linkSystemLibrary("comctl32", .{});
|
|
app_mod.linkSystemLibrary("ole32", .{});
|
|
app_mod.linkSystemLibrary("oleacc", .{});
|
|
app_mod.linkSystemLibrary("shell32", .{});
|
|
// TypeScript cores link ScriptC's host runtime, whose network-interface
|
|
// helpers use GetAdaptersAddresses and Winsock address conversion; the
|
|
// in-process service archive shares the same runtime and additionally
|
|
// reaches advapi32 (crypto-backed randomness).
|
|
app_mod.linkSystemLibrary("iphlpapi", .{});
|
|
app_mod.linkSystemLibrary("ws2_32", .{});
|
|
app_mod.linkSystemLibrary("advapi32", .{});
|
|
// 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", .{});
|
|
}
|
|
}
|
|
|
|
/// Link-search metadata is not inherited through a compiled object in Zig's
|
|
/// build graph. Keep every path-only platform setting in this one helper so a
|
|
/// TypeScript app's cached app-code object and its final link receive the same
|
|
/// framework/library lookup and runtime-search policy.
|
|
fn addPlatformLinkSearchPaths(b: *std.Build, platform: PlatformOption, web_engine: WebEngineOption, cef_dir: []const u8, mod: *std.Build.Module) void {
|
|
if (platform == .macos) {
|
|
if (b.sysroot) |sysroot| {
|
|
mod.addFrameworkPath(.{ .cwd_relative = b.pathJoin(&.{ sysroot, "System/Library/Frameworks" }) });
|
|
}
|
|
if (web_engine == .chromium) {
|
|
mod.addFrameworkPath(b.path(b.fmt("{s}/Release", .{cef_dir})));
|
|
mod.addRPath(.{ .cwd_relative = "@executable_path/Frameworks" });
|
|
}
|
|
} else if (platform == .linux and web_engine == .chromium) {
|
|
mod.addLibraryPath(b.path(b.fmt("{s}/Release", .{cef_dir})));
|
|
mod.addRPath(.{ .cwd_relative = "$ORIGIN" });
|
|
} else if (platform == .windows and web_engine == .chromium) {
|
|
mod.addLibraryPath(b.path(b.fmt("{s}/Release", .{cef_dir})));
|
|
}
|
|
}
|
|
|
|
/// 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(dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, run: *std.Build.Step.Run, web_engine: WebEngineOption, web_layer: bool) void {
|
|
if (web_engine != .system) return;
|
|
if (!web_layer) return;
|
|
if (target.result.os.tag != .windows) return;
|
|
const loader_dir = std.fs.path.dirname(webView2LoaderSubPath(target)).?;
|
|
run.addPathDir(dep.builder.pathFromRoot(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 "Fix with: native cef install --dir {s}" >&2
|
|
\\ exit 1
|
|
\\}}
|
|
, .{ 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 });
|
|
}
|
|
|
|
/// What the build graph reads out of app.zon: the web-engine/CEF knobs
|
|
/// and the web-layer inference inputs. An unreadable or unparsable
|
|
/// manifest falls back to the system engine WITH the web layer kept —
|
|
/// over-inclusion is a size cost, wrong exclusion is a broken app.
|
|
const AppManifestBuildConfig = struct {
|
|
app_id: []const u8 = "dev.native_sdk.app",
|
|
web_engine: WebEngineOption = .system,
|
|
cef_dir: []const u8 = "third_party/cef/macos",
|
|
cef_auto_install: bool = false,
|
|
webview_layer: WebLayerOption = .auto,
|
|
dock_visible: bool = true,
|
|
microphone_permission: bool = false,
|
|
system_audio_permission: bool = false,
|
|
persist_capability: bool = false,
|
|
persist_version: ?u64 = null,
|
|
service_packages: []const ServicePackageConfig = &.{},
|
|
service_carrier: []const u8 = "auto",
|
|
service_pool_size: ?u8 = null,
|
|
store_capability: bool = false,
|
|
relational_capability: bool = false,
|
|
credentials_capability: bool = false,
|
|
credentials_permission: bool = false,
|
|
filesystem_permission: bool = false,
|
|
max_image_pixel_bytes: usize = 1024 * 1024,
|
|
updates_enabled: bool = false,
|
|
sqlite_capability: bool = false,
|
|
/// The first web declaration found (for teaching messages), or null
|
|
/// when app.zon declares no web use. `web_engine = "system"` alone is
|
|
/// NOT web intent — it is the default in many canvas manifests.
|
|
web_declaration: ?web_layer_contract.Declaration = null,
|
|
};
|
|
|
|
const ServicePackageConfig = struct {
|
|
name: []const u8,
|
|
version: []const u8,
|
|
content_hash: []const u8,
|
|
};
|
|
|
|
/// The lenient app.zon shape the build graph parses for inference: web
|
|
/// inclusion, permissions, and the persistence capability/version handed to
|
|
/// the TypeScript checker. Everything else is ignored. Full schema validation
|
|
/// stays with `native validate` and the runner's comptime import.
|
|
const InferenceManifest = struct {
|
|
id: []const u8 = "dev.native_sdk.app",
|
|
capabilities: []const []const u8 = &.{},
|
|
permissions: []const []const u8 = &.{},
|
|
dock_visible: bool = true,
|
|
web_engine: []const u8 = "system",
|
|
webview_layer: []const u8 = "auto",
|
|
core_compiler: []const u8 = "external",
|
|
cef: struct {
|
|
dir: []const u8 = "third_party/cef/macos",
|
|
auto_install: bool = false,
|
|
} = .{},
|
|
frontend: ?struct {} = null,
|
|
persist: ?struct {
|
|
version: u64,
|
|
} = null,
|
|
images: struct {
|
|
max_image_pixel_bytes: usize = 1024 * 1024,
|
|
} = .{},
|
|
service_packages: []const ServicePackageConfig = &.{},
|
|
service_carrier: []const u8 = "auto",
|
|
service_pool_size: u8 = 0,
|
|
updates: ?struct {} = null,
|
|
shell: struct {
|
|
windows: []const struct {
|
|
views: []const struct {
|
|
kind: []const u8 = "",
|
|
} = &.{},
|
|
} = &.{},
|
|
} = .{},
|
|
};
|
|
|
|
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,
|
|
};
|
|
}
|
|
|
|
/// Resolve an app-relative path against `app_root` (see AppOptions). Kept
|
|
/// lexical: `b.path` rejects absolute paths and the generated build graph
|
|
/// hands us "../..", which openat/b.path both resolve fine.
|
|
fn appPath(b: *std.Build, app_root: []const u8, sub_path: []const u8) []const u8 {
|
|
if (app_root.len == 0 or std.mem.eql(u8, app_root, ".")) return sub_path;
|
|
return b.pathJoin(&.{ app_root, sub_path });
|
|
}
|
|
|
|
fn appManifestBuildConfig(b: *std.Build, app_root: []const u8, manifest_name: []const u8) AppManifestBuildConfig {
|
|
// The fallback for a manifest this lenient parse cannot read keeps
|
|
// the web layer (see AppManifestBuildConfig): a shape mismatch here
|
|
// is not proof the app declares no web use.
|
|
const fallback: AppManifestBuildConfig = .{ .web_declaration = .unreadable_manifest };
|
|
const source = b.build_root.handle.readFileAlloc(b.graph.io, appPath(b, app_root, manifest_name), b.allocator, .limited(1024 * 1024)) catch return fallback;
|
|
@setEvalBranchQuota(2000);
|
|
const raw = if (std.ascii.eqlIgnoreCase(std.fs.path.extension(manifest_name), ".json"))
|
|
std.json.parseFromSliceLeaky(InferenceManifest, b.allocator, source, .{ .ignore_unknown_fields = true }) catch return fallback
|
|
else zon: {
|
|
const source_z = b.allocator.dupeZ(u8, source) catch return fallback;
|
|
break :zon std.zon.parse.fromSliceAlloc(InferenceManifest, b.allocator, source_z, null, .{ .ignore_unknown_fields = true }) catch return fallback;
|
|
};
|
|
// `.core_compiler` names the one lane there is; validated here so
|
|
// the removed transpiled lane's spelling teaches at configure time.
|
|
if (!std.mem.eql(u8, raw.core_compiler, "external")) {
|
|
if (std.mem.eql(u8, raw.core_compiler, "transpiler")) @panic(core_compiler_teaching);
|
|
@panic("\napp.zon .core_compiler must be \"external\" (the default and only lane)\n");
|
|
}
|
|
return .{
|
|
.app_id = raw.id,
|
|
.web_engine = web_layer_contract.parseWebEngine(raw.web_engine) orelse .system,
|
|
.cef_dir = raw.cef.dir,
|
|
.cef_auto_install = raw.cef.auto_install,
|
|
.webview_layer = web_layer_contract.parseWebViewLayer(raw.webview_layer) orelse @panic("app.zon .webview_layer must be \"auto\", \"include\", or \"exclude\""),
|
|
.dock_visible = raw.dock_visible,
|
|
.microphone_permission = hasManifestPermission(raw.permissions, "microphone"),
|
|
.system_audio_permission = hasManifestPermission(raw.permissions, "system_audio"),
|
|
.persist_capability = hasManifestCapability(raw.capabilities, "persist"),
|
|
.persist_version = if (raw.persist) |persist| persist.version else null,
|
|
.service_packages = raw.service_packages,
|
|
.service_carrier = raw.service_carrier,
|
|
.service_pool_size = if (raw.service_pool_size == 0) null else raw.service_pool_size,
|
|
.store_capability = hasManifestCapability(raw.capabilities, "store"),
|
|
.relational_capability = hasManifestCapability(raw.capabilities, "sqlite"),
|
|
.credentials_capability = hasManifestCapability(raw.capabilities, "credentials"),
|
|
.credentials_permission = hasManifestPermission(raw.permissions, "credentials"),
|
|
.filesystem_permission = hasManifestPermission(raw.permissions, "filesystem"),
|
|
.max_image_pixel_bytes = raw.images.max_image_pixel_bytes,
|
|
.updates_enabled = raw.updates != null,
|
|
.sqlite_capability = hasManifestCapability(raw.capabilities, "store") or hasManifestCapability(raw.capabilities, "sqlite"),
|
|
.web_declaration = web_layer_contract.manifestDeclaration(raw),
|
|
};
|
|
}
|
|
|
|
fn hasManifestPermission(permissions: []const []const u8, expected: []const u8) bool {
|
|
for (permissions) |permission| {
|
|
if (std.mem.eql(u8, permission, expected)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
fn hasManifestCapability(capabilities: []const []const u8, expected: []const u8) bool {
|
|
for (capabilities) |capability| {
|
|
if (std.mem.eql(u8, capability, expected)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// The web-layer decision for this build: the shared contract fed this
|
|
/// boundary's inputs — the manifest declarations from the lenient parse
|
|
/// and the engine RESOLVED from `-Dweb-engine` orelse app.zon. WEB means
|
|
/// the embedded-WebView layer compiles in; NATIVE-ONLY compiles the host
|
|
/// without it. `.webview_layer` (and `-Dweb-layer`) override the
|
|
/// inference — but an exclude that contradicts a web declaration is a
|
|
/// hard configure error, never a silently broken app.
|
|
fn resolveWebLayer(config: AppManifestBuildConfig, web_engine: WebEngineOption, override: ?WebLayerOption) bool {
|
|
const setting = override orelse config.webview_layer;
|
|
const declaration = web_layer_contract.foldEngine(config.web_declaration, web_engine);
|
|
const decision = web_layer_contract.decide(setting, declaration) catch std.debug.panic(
|
|
"the web layer is excluded ({s}) but the app declares web use ({s}); remove the exclude or drop the web declaration",
|
|
.{ if (override != null) "-Dweb-layer=exclude" else "app.zon .webview_layer = \"exclude\"", declaration.?.text() },
|
|
);
|
|
return decision.enabled;
|
|
}
|