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#"
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