Skip to content

Commit 9b4f62d

Browse files
ctateWhiteHades
andauthored
Infer the web layer and enforce native-only builds end to end (#107)
* Infer the web layer and enforce native-only builds end to end - The build graph parses app.zon and strips the Windows webview layer, loader staging, and dev PATH wiring when nothing declares web use; a webview_layer manifest field and -Dweb-layer flag override inference in both directions - Conflicting declarations are rejected with one teaching message at validate, configure, runner compile, and package time, and a native-only build that reaches webview creation fails fast with WebViewLayerNotBuilt instead of a blank window - A PE cross-audit build step pins that native-only Windows exes never reference the loader while webview apps must, and native check prints the web-layer verdict Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com> * Unify web-layer inference behind one shared contract - app_manifest.web_layer owns the declaration scan, engine folding, and include/exclude decision, usable at comptime by the runner and at runtime by the build graph, validator, CLI, and generated scaffolds, with boundary ownership documented where each adapter lives - Packaging decides from the resolved engine so --web-engine overrides cannot skew the layer, the runner guard covers shell views and manifest chromium, and the full scaffold emits the same inference, conflict panic, and conditional Windows wiring as the SDK graph - A contract matrix test runs every manifest shape through every boundary form so the definitions can never diverge again * Document the webview_layer override and Chromium web intent - The capabilities page counts a Chromium-resolved engine as web intent and points at the override; the app.zon reference gains the webview_layer field, its inference and include/exclude semantics, and the exclude-conflict rule with the shipped error's remedy * Carry the web-layer resolution into packaging - Both build graphs forward their computed web-layer decision to native package via a new --web-layer flag, so the exe and the package can never disagree; a confirming flag keeps the manifest's reason while an overriding one names itself - Packaging PE-scans Windows binaries and refuses to package a loader-referencing exe under a loaderless decision, closing the mismatch for hand-built binaries too - Fixes an adjacent buildgraph bug where a sentinel-terminated path allocation was returned as a plain slice * Honor the webview stub define before header visibility - NATIVE_SDK_ALLOW_WEBVIEW2_STUB now excludes the embedded layer even when WebView2 headers are globally visible, so a native-only build can never reintroduce the loader reference; the vendor pins lock the guard order - The stub message says the layer is excluded by configuration instead of claiming the header is missing --------- Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com>
1 parent edbb204 commit 9b4f62d

28 files changed

Lines changed: 1765 additions & 157 deletions

.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ jobs:
130130
- name: Install GTK dependencies
131131
run: sudo apt-get update && sudo apt-get install -y libgtk-4-dev libwebkitgtk-6.0-dev
132132
- run: zig build test-examples-native
133+
# Declare-to-use, proven on real Windows executables: the
134+
# canvas-only ui-inbox cross-compiles without the embedded WebView
135+
# layer (no WebView2Loader.dll reference, no loader installed) and
136+
# the webview example keeps it.
137+
- run: zig build test-windows-web-layer-audit
133138

134139
linux-canvas-smoke:
135140
name: Linux Canvas Smoke

build.zig

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -482,19 +482,31 @@ pub fn build(b: *std.Build) void {
482482
.{ .path = "src/platform/linux/gtk_host.c", .pattern = "change != NATIVE_SDK_GST_STATE_CHANGE_ASYNC" },
483483
.{ .path = "src/platform/linux/gtk_host.c", .pattern = "#define NATIVE_SDK_GST_STATE_CHANGE_ASYNC 2" },
484484
});
485-
// The embedded-WebView layer must stay real: the vendored WebView2
486-
// SDK header turns the guard on, every first-party build graph puts
487-
// it on the include path, and a build that cannot see it fails at
488-
// compile time instead of quietly shipping the stubbed host (whose
489-
// WebView loads report WebViewNotFound at runtime).
490-
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-windows-webview2-vendor", "Verify the vendored WebView2 SDK stays wired into every Windows build graph", &.{
485+
// The embedded-WebView layer must stay real AND declare-to-use: the
486+
// standard build graph puts the vendored WebView2 SDK header on the
487+
// include path exactly when app.zon declares web use (`if
488+
// (web_layer)`), and only the native-only branch passes the stub
489+
// define. In the guard the stub define is tested FIRST, before
490+
// header visibility: on a machine where the WebView2 SDK headers are
491+
// reachable through the system include paths, an
492+
// include-path-decides guard would compile the full layer into a
493+
// native-only build and reintroduce the WebView2Loader.dll reference
494+
// its executable must not carry. Without the define the header is
495+
// required — a web build that cannot see it fails at compile time
496+
// instead of quietly shipping the stubbed host (whose WebView loads
497+
// report WebViewNotFound at runtime).
498+
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-windows-webview2-vendor", "Verify the vendored WebView2 SDK stays wired into every web-declaring Windows build graph", &.{
491499
.{ .path = "third_party/webview2/include/WebView2.h", .pattern = "CreateCoreWebView2EnvironmentWithOptions" },
492500
.{ .path = "third_party/webview2/include/EventToken.h", .pattern = "EventRegistrationToken" },
493501
.{ .path = "third_party/webview2/LICENSE.txt", .pattern = "Redistribution and use in source and binary forms" },
502+
.{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "#if defined(NATIVE_SDK_ALLOW_WEBVIEW2_STUB)" },
503+
.{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "#elif __has_include(<WebView2.h>) && __has_include(<wrl.h>)" },
494504
.{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "#error \"WebView2.h not found" },
495505
.{ .path = "src/platform/windows/webview2_host.cpp", .pattern = "LoadLibraryW(L\"WebView2Loader.dll\")" },
506+
.{ .path = "build/app.zig", .pattern = ".system => if (web_layer) {" },
496507
.{ .path = "build/app.zig", .pattern = "app_mod.addIncludePath(dep.path(\"third_party/webview2/include\"));" },
497508
.{ .path = "build/app.zig", .pattern = "third_party/webview2/x64/WebView2Loader.dll" },
509+
.{ .path = "build/app.zig", .pattern = "\"-DNATIVE_SDK_ALLOW_WEBVIEW2_STUB\"" },
498510
.{ .path = "src/tooling/templates.zig", .pattern = "third_party/webview2/include" },
499511
.{ .path = "src/tooling/templates.zig", .pattern = "third_party/webview2/x64/WebView2Loader.dll" },
500512
});
@@ -797,6 +809,47 @@ pub fn build(b: *std.Build) void {
797809
const browser_system_link_step = b.step("test-browser-system-link", "Build the browser example with the system engine");
798810
browser_system_link_step.dependOn(&build_browser_system.step);
799811

812+
// Windows web-layer PE cross-audit: the declare-to-use inference,
813+
// proven on real executables from any host via cross-compile.
814+
// ui-inbox's manifest declares no web use, so its exe must carry no
815+
// WebView2Loader.dll reference (the whole embedded layer compiles
816+
// out) and no loader may land beside it; the webview example declares
817+
// the "webview" capability, so its exe must keep the reference. The
818+
// loader string lives as UTF-16 in the host, so the audit is a
819+
// dedicated scanner (tools/audit_web_layer.zig), not a text grep.
820+
const web_layer_auditor = b.addExecutable(.{
821+
.name = "audit-web-layer",
822+
.root_module = module(b, host_target, optimize, "tools/audit_web_layer.zig"),
823+
});
824+
// A loader installed by a build predating the inference would fail
825+
// the absence check below without being this build's fault; clear it
826+
// so the assertion tests what THIS build installs.
827+
const clean_native_only_loader = b.addSystemCommand(&.{ "sh", "-c", "rm -f examples/ui-inbox/zig-out/bin/WebView2Loader.dll" });
828+
const build_native_only_windows = b.addSystemCommand(&.{ "zig", "build", "-Dtarget=x86_64-windows-gnu", "-Dplatform=windows" });
829+
build_native_only_windows.setCwd(b.path("examples/ui-inbox"));
830+
build_native_only_windows.step.dependOn(&clean_native_only_loader.step);
831+
const build_webview_windows = b.addSystemCommand(&.{ "zig", "build", "-Dtarget=x86_64-windows-gnu", "-Dplatform=windows", "-Dweb-engine=system" });
832+
build_webview_windows.setCwd(b.path("examples/webview"));
833+
const audit_native_only_exe = b.addRunArtifact(web_layer_auditor);
834+
audit_native_only_exe.addArgs(&.{ "examples/ui-inbox/zig-out/bin/ui-inbox.exe", "absent" });
835+
audit_native_only_exe.has_side_effects = true;
836+
audit_native_only_exe.step.dependOn(&build_native_only_windows.step);
837+
const audit_webview_exe = b.addRunArtifact(web_layer_auditor);
838+
audit_webview_exe.addArgs(&.{ "examples/webview/zig-out/bin/webview.exe", "present" });
839+
audit_webview_exe.has_side_effects = true;
840+
audit_webview_exe.step.dependOn(&build_webview_windows.step);
841+
const audit_native_only_loader = b.addSystemCommand(&.{ "sh", "-c",
842+
\\test ! -f examples/ui-inbox/zig-out/bin/WebView2Loader.dll || {
843+
\\ echo "web-layer audit FAILED: the native-only build installed WebView2Loader.dll" >&2
844+
\\ exit 1
845+
\\}
846+
});
847+
audit_native_only_loader.step.dependOn(&build_native_only_windows.step);
848+
const web_layer_audit_step = b.step("test-windows-web-layer-audit", "Cross-compile a native-only and a web example for Windows and audit the web layer in each exe");
849+
web_layer_audit_step.dependOn(&audit_native_only_exe.step);
850+
web_layer_audit_step.dependOn(&audit_webview_exe.step);
851+
web_layer_audit_step.dependOn(&audit_native_only_loader.step);
852+
800853
const frontend_examples_step = b.step("test-examples-frontends", "Run frontend example tests");
801854
addExampleTestStep(b, host_cli_exe, frontend_examples_step, "test-example-next", "Run Next example tests", "examples/next", .owned);
802855
addExampleTestStep(b, host_cli_exe, frontend_examples_step, "test-example-react", "Run React example tests", "examples/react", .owned);

0 commit comments

Comments
 (0)