Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/actions/headless-host/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
23 changes: 21 additions & 2 deletions apps/chuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions apps/chuzz/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
31 changes: 1 addition & 30 deletions apps/chuzz/src/browser.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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#"<!doctype html><html><head><meta charset="utf-8"><title></title></head><body style="margin:0;background:transparent"></body></html>"#;
/// 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#"<!doctype html><html><head><meta charset="utf-8"><title>Empty response</title></head><body style="margin:0;background:#f6f6f7;color:#16181d"><h1>Empty response</h1><p>The server returned no content.</p></body></html>"#;

Expand Down Expand Up @@ -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 `<pre>`, 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('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;");
format!(
r#"<!doctype html><html><head><meta charset="utf-8"><title>Source</title></head>
<body style="{INTERNAL_PAGE_STYLE}"><pre style="margin:0;padding:1rem;font:13px ui-monospace,monospace;white-space:pre-wrap;word-break:break-word">{escaped}</pre></body></html>"#
)
}

pub(crate) fn page_node(document: &blitz_dom::BaseDocument, tab_id: u64) -> Option<NodeId> {
if let Some(node) = document.get_element_by_id(&format!("chuzz-page-{tab_id}")) {
return Some(node);
Expand Down
2 changes: 1 addition & 1 deletion apps/chuzz/src/capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ mod tests {
// A page whose rendered form is unmistakably different from its source:
// an <h1> would paint large and bold, and the tags would vanish.
const PAGE: &str = "<h1>Example Domain</h1><p>a &amp; b</p>";
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.
Expand Down
2 changes: 1 addition & 1 deletion apps/chuzz/src/document_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
38 changes: 38 additions & 0 deletions apps/chuzz/src/internal_pages.rs
Original file line number Diff line number Diff line change
@@ -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 `<pre>`, 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('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;");
format!(
r#"<!doctype html><html><head><meta charset="utf-8"><title>Source</title></head>
<body style="{INTERNAL_PAGE_STYLE}"><pre style="margin:0;padding:1rem;font:13px ui-monospace,monospace;white-space:pre-wrap;word-break:break-word">{escaped}</pre></body></html>"#
)
}
8 changes: 8 additions & 0 deletions apps/chuzz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
Loading