From 00abec2037c9192a5493f70bba0ba3944c121405 Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 9 Sep 2026 17:01:44 +0700 Subject: [PATCH 1/2] fix(headless): no Tauri, and so no GTK, in a browser with no window `cargo build --bin chuzz-headless` failed on a Linux runner with The system library `glib-2.0` required by crate `glib-sys` was not found. Dependencies are per package, not per binary, so the headless binary compiled the whole of `chuzz-gui`'s graph. `default-features = false` on `tauri` was not enough, and could not be: the path is not the webview. glib-sys <- atk <- gtk <- muda <- tauri gtk <- webkit2gtk <- tauri-runtime `muda` is Tauri's menu crate and `tauri-runtime` names WKWebView types in its public API, so both are unconditional. Same shape as the `objc2-web-kit` note in build.rs, which is about the macOS end of the same fact. `tauri-runtime-blitz` already anticipated this: its `runtime` feature gates Tauri, and its own comment says that off it, "a headless consumer gets the inspection and activation surface with no Tauri in its graph, which on Linux is the difference between a build and GTK development headers for a binary that never opens a window". This workspace was taking it with defaults and getting the window stack it does not use. So `gui` is now a feature: `dep:tauri` and `tauri-runtime-blitz/runtime`, with `browser`, `frontend` and the `chuzz-gui` binary behind it, and `tauri_build` gated in build.rs because the context it generates has nothing to describe without them. Default keeps it on, so an ordinary build is unchanged. `source_html` moved to a new `internal_pages` module. It is a pure string function that `document_loader` and `capture` both call, and it was the only thing keeping `browser` in the headless graph. View source and the error page are pages rather than chrome, so that is where they belonged anyway. Verified against the Linux target: `cargo tree -i glib-sys` and `-i tauri` both report no such package in the headless graph, and the GUI binary still builds. 0.1.37 rather than a second pull request: the action changes how consumers build the host and the feature split changes what ships, so they are one release. --- .github/actions/headless-host/action.yml | 7 ++++- Cargo.toml | 7 +++-- apps/chuzz/Cargo.toml | 23 ++++++++++++-- apps/chuzz/build.rs | 5 ++++ apps/chuzz/src/browser.rs | 31 +------------------ apps/chuzz/src/capture.rs | 2 +- apps/chuzz/src/document_loader.rs | 2 +- apps/chuzz/src/internal_pages.rs | 38 ++++++++++++++++++++++++ apps/chuzz/src/lib.rs | 8 +++++ 9 files changed, 86 insertions(+), 37 deletions(-) create mode 100644 apps/chuzz/src/internal_pages.rs diff --git a/.github/actions/headless-host/action.yml b/.github/actions/headless-host/action.yml index 8f600ac..887d3e1 100644 --- a/.github/actions/headless-host/action.yml +++ b/.github/actions/headless-host/action.yml @@ -72,5 +72,10 @@ runs: id: build shell: bash run: | - cargo build --release --manifest-path .qa-host/Cargo.toml --bin chuzz-headless + # `--no-default-features` drops `gui`, and with it `tauri`, `muda -> gtk` + # and `tauri-runtime -> webkit2gtk`. A Linux runner then needs no GTK + # development headers to build a browser that never opens a window. + cargo build --release --manifest-path .qa-host/Cargo.toml \ + --bin chuzz-headless --no-default-features \ + --features capture,javascript,vello,scrollbars,webp echo "host=$PWD/.qa-host/target/release/chuzz-headless" >> "$GITHUB_OUTPUT" diff --git a/Cargo.toml b/Cargo.toml index 71f1222..87791f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["apps/chuzz", "crates/chuzz-control"] resolver = "3" [workspace.package] -version = "0.1.36" +version = "0.1.37" edition = "2024" rust-version = "1.91" license = "MIT OR Apache-2.0" @@ -118,7 +118,10 @@ tauri = { version = "^2.11.5", default-features = false } # explaining. A window that would not paint could not be photographed by the # one thing that could photograph it, because the capability had been optimised # out of the binary that needed it. -tauri-runtime-blitz = { version = "^0.3.2", features = ["diagnostics"] } +tauri-runtime-blitz = { version = "^0.3.7", default-features = false, features = [ + "agent-control", + "diagnostics", +] } [profile.release] lto = "thin" diff --git a/apps/chuzz/Cargo.toml b/apps/chuzz/Cargo.toml index e7da650..27e64c4 100644 --- a/apps/chuzz/Cargo.toml +++ b/apps/chuzz/Cargo.toml @@ -14,6 +14,12 @@ publish = false [[bin]] name = "chuzz-gui" path = "src/tauri_main.rs" +# The window, and the only thing that needs Tauri. Off this feature `tauri` +# leaves the graph, and with it `muda` -> `gtk` -> `glib-sys` and +# `tauri-runtime` -> `webkit2gtk`. Neither is reachable from a binary that +# never opens a window, and on Linux their absence is the difference between +# building and needing GTK development headers. +required-features = ["gui"] # The same browser with no window, serving one page over the inspection socket # for `ps-qa` to drive. See `src/serve.rs` for why it is a mode of this binary's @@ -31,7 +37,20 @@ required-features = ["capture", "javascript"] [features] # The renderer choice follows AgencyZero's Blitz performance thesis: Vello on # wgpu, so macOS gets Metal and Linux/Windows keep Vulkan and D3D12 later. -default = ["vello", "scrollbars", "javascript", "wasm", "capture", "webp", "avif"] +default = [ + "gui", + "vello", + "scrollbars", + "javascript", + "wasm", + "capture", + "webp", + "avif", +] +# The window and its Tauri surface: `browser`, `frontend` and the `chuzz-gui` +# binary. A headless build turns this off and keeps the engine, the loader, the +# rasteriser and the inspection socket, which is everything ps-qa drives. +gui = ["dep:tauri", "tauri-runtime-blitz/runtime"] # Let a WebAssembly guest build a page, through `--wasm` in the window and # `--capture-wasm` headlessly. On by default because it is a way of opening a # page rather than a diagnostic: with it off, `--wasm` is not a flag the binary @@ -125,7 +144,7 @@ tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] } url.workspace = true serde.workspace = true serde_json.workspace = true -tauri.workspace = true +tauri = { workspace = true, optional = true } tauri-runtime-blitz.workspace = true tokio-tungstenite.workspace = true futures-util.workspace = true diff --git a/apps/chuzz/build.rs b/apps/chuzz/build.rs index 8d6ae9d..03f37e1 100644 --- a/apps/chuzz/build.rs +++ b/apps/chuzz/build.rs @@ -171,5 +171,10 @@ fn main() { strip_unused_frameworks(); stamp_build(); build_frontend(); + // Generates the Tauri context, which only the `chuzz-gui` binary consumes. + // A headless build has no `tauri` in its graph for the context to describe, + // and running this anyway is what made `cargo build --bin chuzz-headless` + // need a Tauri toolchain on a machine with no window system. + #[cfg(feature = "gui")] tauri_build::build(); } diff --git a/apps/chuzz/src/browser.rs b/apps/chuzz/src/browser.rs index 3c13d13..e5a205c 100644 --- a/apps/chuzz/src/browser.rs +++ b/apps/chuzz/src/browser.rs @@ -1,6 +1,7 @@ use std::collections::{HashMap, VecDeque}; use std::sync::{Arc, Mutex, Weak}; +use crate::internal_pages::{INTERNAL_PAGE_STYLE, source_html}; use blitz_dom::{Document as _, DocumentConfig, FontContext, NodeId}; use blitz_html::HtmlProvider; use blitz_traits::navigation::{NavigationOptions, NavigationProvider}; @@ -21,18 +22,6 @@ use crate::nav::{NEW_TAB_URL, display_title, request_from_input}; /// tab stayed blue whatever they picked. The shell paints `.page` with the /// themed surface and this lets it through. const BLANK_HTML: &str = r#""#; -/// Colours for the pages the browser writes itself. -/// -/// Explicit, and light, like every other browser's error and source pages. -/// These documents declare no colours of their own, so they inherited the -/// engine's defaults: black text on a transparent background, over a viewport -/// the shell paints with the dark theme surface. The source of a page was -/// therefore rendered, laid out, and unreadable, which is indistinguishable -/// from not being rendered at all and was reported as exactly that. -/// -/// Not a theme token. These are documents in a page viewport, not part of the -/// chrome, and nothing in a page can reach the shell's custom properties. -const INTERNAL_PAGE_STYLE: &str = "margin:0;background:#f6f6f7;color:#16181d"; const EMPTY_HTML: &str = r#"Empty response

Empty response

The server returned no content.

"#; @@ -876,24 +865,6 @@ async fn fetch_page_module( }) } -/// Wrap a server's bytes in the smallest document that shows them verbatim. -/// -/// Escaped and put in a `
`, which is the whole job: the point of view
-/// source is that what you read is what arrived, so nothing here may reformat,
-/// pretty-print or re-serialise it. A document that showed a parsed and
-/// re-emitted tree would be answering a different question, and for a page
-/// whose claim is "there is no script here" it would be the wrong answer.
-pub(crate) fn source_html(text: &str) -> String {
-    let escaped = text
-        .replace('&', "&")
-        .replace('<', "<")
-        .replace('>', ">");
-    format!(
-        r#"Source
-
{escaped}
"# - ) -} - pub(crate) fn page_node(document: &blitz_dom::BaseDocument, tab_id: u64) -> Option { if let Some(node) = document.get_element_by_id(&format!("chuzz-page-{tab_id}")) { return Some(node); diff --git a/apps/chuzz/src/capture.rs b/apps/chuzz/src/capture.rs index b7d3663..538d4be 100644 --- a/apps/chuzz/src/capture.rs +++ b/apps/chuzz/src/capture.rs @@ -473,7 +473,7 @@ mod tests { // A page whose rendered form is unmistakably different from its source: // an

would paint large and bold, and the tags would vanish. const PAGE: &str = "

Example Domain

a & b

"; - let html = crate::browser::source_html(PAGE); + let html = crate::internal_pages::source_html(PAGE); // The escaping is the contract the picture depends on, so assert it // before painting: a failure here explains a failure below. diff --git a/apps/chuzz/src/document_loader.rs b/apps/chuzz/src/document_loader.rs index 773a5dc..d1125ff 100644 --- a/apps/chuzz/src/document_loader.rs +++ b/apps/chuzz/src/document_loader.rs @@ -1214,7 +1214,7 @@ pub async fn load_for_capture( .fetch_async(Request::get(url)) .await .map_err(|error| format!("could not fetch {inner}: {error:?}"))?; - let html = crate::browser::source_html(&decode_body(&bytes)); + let html = crate::internal_pages::source_html(&decode_body(&bytes)); return Ok(CapturedDocument::Html(Box::new( blitz_html::HtmlDocument::from_html( &html, diff --git a/apps/chuzz/src/internal_pages.rs b/apps/chuzz/src/internal_pages.rs new file mode 100644 index 0000000..8d0b052 --- /dev/null +++ b/apps/chuzz/src/internal_pages.rs @@ -0,0 +1,38 @@ +//! The documents the browser writes itself. +//! +//! View source and the error page are pages, not chrome: they are built as +//! HTML and loaded into a viewport like anything else. That makes them +//! reachable from the headless host as well as the window, which is why they +//! live here rather than in `browser`, whose Tauri command surface a headless +//! build does not compile. + +/// Colours for the pages the browser writes itself. +/// +/// Explicit, and light, like every other browser's error and source pages. +/// These documents declare no colours of their own, so they inherited the +/// engine's defaults: black text on a transparent background, over a viewport +/// the shell paints with the dark theme surface. The source of a page was +/// therefore rendered, laid out, and unreadable, which is indistinguishable +/// from not being rendered at all and was reported as exactly that. +/// +/// Not a theme token. These are documents in a page viewport, not part of the +/// chrome, and nothing in a page can reach the shell's custom properties. +pub(crate) const INTERNAL_PAGE_STYLE: &str = "margin:0;background:#f6f6f7;color:#16181d"; + +/// A page's own source, as a document. +/// +/// Escaped and put in a `
`, which is the whole job: the point of view
+/// source is that what you read is what arrived, so nothing here may reformat,
+/// pretty-print or re-serialise it. A document that showed a parsed and
+/// re-emitted tree would be answering a different question, and for a page
+/// whose claim is "there is no script here" it would be the wrong answer.
+pub(crate) fn source_html(text: &str) -> String {
+    let escaped = text
+        .replace('&', "&")
+        .replace('<', "<")
+        .replace('>', ">");
+    format!(
+        r#"Source
+
{escaped}
"# + ) +} diff --git a/apps/chuzz/src/lib.rs b/apps/chuzz/src/lib.rs index f6900c5..fa63d6e 100644 --- a/apps/chuzz/src/lib.rs +++ b/apps/chuzz/src/lib.rs @@ -14,6 +14,9 @@ //! the harness, so the harness measured a browser nobody ships. There is one //! loader now, and one place a gap gets fixed. +// The window and its Tauri command surface. Behind `gui` because `tauri` is, +// and because a headless build has no window to drive. +#[cfg(feature = "gui")] pub mod browser; #[cfg(feature = "capture")] pub mod capture; @@ -23,8 +26,13 @@ pub mod document_loader; // pixels have nothing to be explained by. #[cfg(feature = "capture")] pub mod dump; +// Draws the browser chrome, which only exists when there is a window. +#[cfg(feature = "gui")] pub mod frontend; pub mod identity; +// The browser's own documents: view source, the error page. Pages rather than +// chrome, so the headless host reaches them too. +pub mod internal_pages; pub mod nav; pub mod net_bridge; // A built site needs an origin before it is a site. Only the headless host From 1a7025b21732dd2e3c9e61a8ce64f4d2a9bcf9fe Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 9 Sep 2026 17:06:16 +0700 Subject: [PATCH 2/2] fix(release): repair the indentation that made release.yml invalid The lockfile commit removed a comment paragraph above the frontend install step and took the step's indentation with it, leaving - name: Install frontend dependencies twelve spaces deep inside a six-space block. GitHub rejects the file before running anything: This run likely failed because of a workflow file issue. So 0.1.36 was merged, tagged in the manifest, and never published: the release job could not start. A bump with a broken workflow is not a release, and the failure names the file rather than the line, which is why it read as an infrastructure problem rather than a typo. Every workflow and action in this repository now parses. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ad630ef..b0713b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -149,7 +149,7 @@ jobs: # for fourteen releases. It worked on a developer machine because an older # install had hoisted it there by luck, which is also why the local tree # was compiling with 0.1.5 while the lockfile said 0.1.6. - - name: Install frontend dependencies + - name: Install frontend dependencies working-directory: apps/chuzz/frontend run: bun install