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/.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 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