jkunz 99e939017c
Test gate / ship-1 (push) Waiting for needed jobs
Test gate / ship-2 (push) Waiting for needed jobs
Test gate / ship-3 (push) Waiting for needed jobs
Test gate / ship-4 (push) Waiting for needed jobs
Test gate / ship-5 (push) Waiting for needed jobs
Test gate / ship-6 (push) Waiting for needed jobs
Test gate / tests-1 (push) Waiting for needed jobs
Test gate / tests-2 (push) Waiting for needed jobs
Test gate / tests-3 (push) Waiting for needed jobs
Test gate / tests-4 (push) Waiting for needed jobs
Test gate / tests-5 (push) Waiting for needed jobs
Test gate / tests-6 (push) Waiting for needed jobs
Test gate / report (push) Waiting for needed jobs
Test gate / gate-1 (push) In progress
On Linux too: with the window's buttons on the leading side, the sidebar's first row starts after them
Some Linux desktops put the window's buttons on the leading side, as macOS does, and 0025's caption
width is what keeps the sidebar's first row clear of them there as well.
WindowBrowserTest.TheFirstRowStartsAfterLeadingWindowButtons sets that order and checks that the
toggle, the AI button and the toolbar all start after the window's buttons. Without 0025 it fails
3 of 3 (the toggle at x=22, the AI button at 50 and the toolbar at 78, under buttons that end at 110);
with it, it passes 8 of 8.
2026-11-09 15:46:18 +00:00
2026-11-09 15:46:18 +00:00
2026-11-08 21:31:11 +00:00

biq.io/chromium

biq's Chromium: the patches we carry on Chromium, the code of the biq window, and the pipeline that builds, tests and releases both. biq ships it, and @push.rocks/smartbrowser Paint uses the same builds.

Layout

Path What it is
patches/base/ Patches every build carries: branding, biq's hooks, picture replay. Keep this layer thin.
patches/paint/ Paint capture. Server builds only; these never ship to a user's device.
biq/ The biq window, the agent host and everything else that's ours. It's copied into src/biq, so it isn't a patch.
skia/ The Skia picture patch and the CanvasKit build's patches. Native Chromium and CanvasKit must carry the same bytes.
audio/ The native audio decoder and mixer.
notices/ Licence material that ships with the build.
scripts/ Fetch, apply, compile, link, package and release.

Why two patch layers

Paint capture changes 180 Chromium files and adds 14,853 lines to them, 9,269 of those in components/viz. That's rework every time Chromium moves. Keeping it out of the desktop build means a conflict there can never hold back a desktop security update, and that user devices carry no capture code.

Every patch costs rework on each milestone, so prefer adding files under biq/ to editing Chromium's, and say in each patch why it exists.

Building

scripts/ci.chromium.sh build runs the whole Paint build in a CI runner's Docker daemon, and .gitea/workflows/chromium.yaml calls it. For local work:

scripts/build.chromium.sh apply
scripts/build.chromium.sh compile
scripts/build.chromium.sh link

Set SMARTBROWSER_PAINT_BUILD_CACHE to a disk with about 200 GB free. A cold build needs about 21 minutes to fetch, 109 minutes to compile on 28 CPUs and 11 minutes to link.

biq's Rust comes from biq.io/core, at the commit chromium.json pins as biqCore. scripts/fetch.core.sh puts it at biq/core before biq/ is copied into the tree. To work on both at once, make biq/core a link to your own checkout.

AI agents

biq/browser/agents/ serves agents over MCP: the crates biq-agent and biq-mcp decide, and the C++ there carries the calls out in the browser. biq mcp is the browser's own binary, relaying stdin and stdout to the running browser. Agents are off until a person turns them on, in chrome://agents, and the BiqAgentsCeiling policy limits them.

scripts/test.agents.sh SOURCE_ROOT out/dev

That runs the unit tests and biq/browser/agents/test/e2e.py, which starts a real browser, speaks real MCP to it, answers its questions with mouse clicks, and tries what a page would talk an agent into.

Rolling Chromium

chromium.json is the one place the pins live. scripts/roll.chromium.py stable moves them to the newest Stable, from that release's own DEPS.

scripts/port.check.py REF lists, before a port starts, every patch and hunk of both series that doesn't apply at a Chromium tag or branch (refs/branch-heads/8103), reading only the files they touch from gitiles: no checkout and no runner. A failure that may only follow from an earlier patch's failure in the same file is marked, so the list is the port's work list. scripts/replay.series.py SOURCE patches/paint then checks that the patches apply. It refuses fuzz, because fuzz once put a line in the wrong function. A security update inside a milestone normally applies as it is; a new milestone gets its failing hunks ported by hand, and scripts/audit.series.py OLD_REF patches/paint shows every block that moved, to be read one by one.

biq follows Chromium's Extended Stable milestones, every fourth one, so a port comes every eight weeks and security updates in between roll in on their own. biq/readme.md says why and what the deadline is.

.gitea/workflows/roll.yaml does the first step every hour, and starts the gate on the branch it opens for Extended Stable's newest tag. A push with the job token starts no workflow, so scripts/roll.start.py dispatches the gate, and makes up for a lost dispatch an hour later. A channel can sit below what we're on, so it never moves the pins back.

The test gate

.gitea/workflows/gate.yaml is what a commit comes through before it's Chromium people run. It starts on main and roll/*, and any other branch can be dispatched. scripts/ci.gate.py holds its stages:

Stages What they prove
filters No line of chromium_browser_tests.filter excludes a test of certificate errors, permissions or a dialog's origin, and tab_sets_browsertest.cc is there, in browser_tests, and selected. See scripts/gate.filters.py. A line can't leave a .required list, nor a - line join one, without a review (below).
brand scripts/test.brand.desktop.py, in the desktop image, which has Pillow. desktop.apply then runs the real thing on a clean checkout.
fetch, replay.* Each patch series applies to the pinned Chromium with no fuzz.
desktop.* The official build compiles, calls itself biq, passes smoke.py in a real window and the agents' end-to-end test, and packages. This is the binary that ships: link-time optimisation and control-flow integrity exist only here.
tests.* The test output (biq/args/dev.gn: a component build, DCHECKs on) passes the same again, the agents' unit tests, and Chromium's own unit_tests, components_unittests, views_unittests, browser_tests and interactive_ui_tests with the filters in biq/browser/ui/test/, tab_sets_browsertest.cc among them. A security update gets the same list as anything else, and a test binary that isn't built fails the run.
paint.* Paint builds officially, its viz, cc, media and content tests pass (scripts/test.paint.filters), and it packages.

The .required lists say what browser_tests and interactive_ui_tests must run, whatever the filter says. Taking a line out of one, or putting a - line into one, leaves out a test of who a person is dealing with, so it needs a second person. The gate's first stage compares the lists with those of the last commit the gate has a note for, and fails the desktop run unless a commit since then carries a trailer for each such line:

Required-Review: chromium_browser_tests.required *WebUsb*.* -- Linus: its tests moved to *Usb*.*

That's the list's file name, the line exactly as it was (or the new - line), two dashes, then who reviewed it and why it can go. One trailer per line. A branch's test.desktop.sh run names the same lines beforehand.

A green test run isn't enough on its own: scripts/gate.results.py also fails a run in which a filter line matched nothing, because a suite Chromium renamed would otherwise take its tests away quietly, one in which any test of tab_sets_browsertest.cc didn't run and pass, and one in which a test a .required file selects needed a retry. The passes of a target (the rest, then .serial one at a time, then .sandboxed with the sandbox on) count as one run, and every test that needed a retry is named at the end.

Desktop and Paint get separate verdicts, because Paint's patches must never hold back a desktop update. Both run whatever the other did. auto reads off the change which of them it touches. On main that's every change since the last commit there that passed each suite, so a run the queue replaced or that was cancelled leaves nothing unproved. A change only to scripts no stage runs, listed one by one in LOGIC_ONLY in scripts/ci.gate.py, needs no suite: the logic tests in the first job prove it, and test.ci.gate.py fails if a stage, or a script a stage runs, calls a listed one. Any other script runs both suites.

A cold gate is more than the three hours the forge allows a job, and its clock is what a security update waits for. So after fetching and replaying, two chains of jobs run at once over one Docker volume: ship builds the official browser and then Paint, tests builds the test binaries and runs them. In a chain each job takes the next stages from a ledger in the volume, Ninja stops at the job's budget and the next job carries on. The runner's limits decide the layout: 32 CPUs, split between the chains, and about 68 GB of disk, against 31 GB of source and 11 to 14 GB per output, so three outputs can't be there at once and the official one goes before Paint starts. Each output's object files and ThinLTO cache go as soon as it has linked, since only a linker reads them and they're most of it: of an official output's 14 GB, 5.6 and 3.1. The note's disk column is the least the disk had free during each stage. Paint's tree is hard links to the first, which costs half a GB. One gate runs at a time, and chromium.yaml waits in the same concurrency group. It can't run beside one, which is also why it follows the gate now instead of a push.

The runner isn't biq's alone: it takes every repository's CI on the forge. Each stage's memory is capped so the two chains fit beside each other, but in run 5971 about 30 GiB of somebody else's work killed Paint's compile at 29 of its 40 GiB. So a stage waits until the runner has its cap free, plus what the gate's other running stage may still grow by, and starts at its normal width when only that fits. After 30 minutes it starts anyway rather than fail a commit for a neighbour. A compile or link the kernel killed below its own cap runs once more at its normal width, and Ninja carries on where it was. A test never runs again: a test that dies under pressure is a result, and its excerpt says the memory went elsewhere. The note records every wait, narrow start and second run. Only a runner of biq's own, or one that runs one job at a time, keeps the neighbours out.

The verdict and the time of each stage stay with the commit, as a git note:

git fetch origin refs/notes/gate:refs/notes/gate
git notes --ref=gate show COMMIT

A commit that already passed isn't gated twice, so a fast-forward merge costs nothing.

The object cache

A security update changes little source, so the test build and Paint's test binaries compile through a ccache in a volume of its own (biq-object-cache), which stays between runs. The official desktop and Paint builds never touch it, in either direction: biq/readme.md says why. scripts/cache.env.sh says what its keys cover: the compiler's revision, the GN args and every file the compiler read. Only this repository's main and roll/* write to it; its other branches read. It's held to 8 GB, a run short of disk switches it off and empties it, and scripts/cc.gate.sh runs the compiler itself whenever the cache can't be used, so a cache problem costs time and never a build.

When a roll needs a person

A failing stage opens an issue named after the Chromium version, one per version: a repeat is a comment. So does a roll that passed, unless it merged itself. Closed on merge. The hourly job has an issue of its own for when it can't run at all.

Automatic merge

The repository variable BIQ_ROLL_AUTOMERGE set to exactly on lets a security-only roll merge itself after a green gate. It's unset, and turning it on is the owner's decision. scripts/roll.security.py is the one place that says what security-only means: same Chromium branch, patch number up; one commit on top of main, changing chromium.json and only the pins that come from Chromium, to exactly what that release's DEPS says; both series replayed with no fuzz; and both suites passed. Anything else waits for a person, who can still merge a desktop update that passed while Paint didn't.

Releases

.smartconfig.json names the Paint Chromium release (@biq.io/chromium.paintChromium.build). A published release is never rebuilt or replaced, so other inputs need a higher build number. chromium.yaml runs after the gate passes on main, when that number names a release that isn't there.

scripts/test.*.py cover the packaging and release logic and need no Chromium checkout.

History

The Paint build moved here from push.rocks/smartbrowser (native/paint, scripts/paint) with its history.

S
Description
biq’s Chromium: the patches, the browser window and the build pipeline. smartbrowser Paint uses it too.
Readme
3.7 MiB
0 Stars 3 Watchers 0 Forks
2026-11-01 20:38:40 +00:00
Languages
C++ 49.9%
Python 35.1%
Shell 7.4%
Rust 6.7%
C 0.7%
Other 0.2%