Skip to content

PDF Tools on Linux: the journeys on a headless weston, and their evidence - #495

Merged
ymote merged 11 commits into
mainfrom
test/pdftools-linux
Oct 11, 2026
Merged

ymote merged 11 commits into
mainfrom
test/pdftools-linux

Conversation

@ymote

@ymote ymote commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Until now PDF Tools v2 had been verified only on macOS. This PR adds a Linux launch path to its end-to-end harness and records the first Linux runs.

  • apps/pdftools/tests/ui.py: on Linux each shell session gets a headless weston of its own (--backend=headless --renderer=pixman --idle-time=0 --no-config, in a private runtime folder). The shell runs on it with Makepad's Wayland backend, in a session of its own, with the run home's XDG folders. /quit ends it, and weston stops after the shell (so does a shell that never answers). macOS still launches from Terminal.app, unchanged.
  • The file dialog on Linux: where no dialog helper is installed, the shell offers no files.import. The runs check "Not available here" there instead of the macOS refusal. Where a helper is on PATH, the dialog would really open, so they leave that step out and say so.
  • Evidence: apps/pdftools/tests/evidence/linux-20261011/ (English and Chinese): frames, Linux-beside-macOS sheets, receipt.json, and import/check.py, the exact import check that ran. The app README's Testing and Status sections are updated in both languages.

No Linux-only bug turned up, so the PR carries no fix.

What ran (Linux build host: Ubuntu 26.04, x86_64, software GL, homes on ext4)

Linux findings

  • File dialog: Makepad's Linux dialog is neither xdg-desktop-portal nor GTK. It runs the desktop's own helper (zenity, qarma, matedialog or kdialog). The headless host has none, so files.status answers import_supported: false, Home shows "Not available here", and the button shows a banner. Nothing hangs. On Linux the window counts as focused, so with a helper installed the dialog really opens. The import passes no file filter on any platform.
  • Fonts and text: the same as macOS (see compare/), although the host has only DejaVu.
  • Software GL: llvmpipe compiles its shaders at startup (stalls of 3.1 to 3.6 s, then 0.5 to 1.2 s). These are outside script handlers. Apart from them, no stall was over 409 ms.
  • Window: the shell's window on weston is 1400 x 900 at dpi 1. PDF Tools opens at 1296 x 698 (an app area of 1292 x 662), the same as the macOS hidden shell. Opened from the menu it is 1296 x 709, above the dock. These sizes were unchanged after The dock hides while OctoSense is full screen, as macOS's Dock does #485 and Maximize and snaps end above a floating dock; honour a native window's requested size #487.
  • Read mode: both passes pass: entered on 2 / 4, left on 3 / 4 at 125%.
  • ext4: saves, render-cache writes and covers ran with no overrun. Covers kept their nanosecond modification times across the restart. The host's /tmp is tmpfs, so pass --work on the disk under test.

Not verified

  • Windows (no machine).
  • A real helper's own dialog. None is installed on the host, and none was installed for this.
  • X11 (weston's Xwayland cannot start there), GNOME or KDE, a GPU, a scale above 1.
  • The card-host runs on Linux, a release build, and CJK text.

Draft: evidence only, please don't merge from here.

🤖 Generated with Claude Code

ymote and others added 11 commits October 11, 2026 09:59
Home's main activity is exported, so any installed app can start it with
intent extras, and Makepad copies `makepad.<NAME>` extras into the
environment. OctoSense code read several of them in release builds.

- shell: `launch_input` is the one reader of MAKEPAD_APP_CONFIG. Its test
  actions, Mail's demo mailbox and module open parameters apply only in a
  development build (cfg(dev_mode): debug or --features dev-mode); a
  release build ignores them and logs that once. Desktop --test-action
  command-line flags are unchanged.
- kernel: the launch proxy (makepad.OCTOS_PROXY) applies only in a
  development build; new `dev-mode` feature, forwarded from the shell
  through ai-host.
- AppCard: every MAKEPAD_* hook (provisioning, seeding, auto prompt, fake
  GPS, proxy, dumps) goes through `launch_var`, development builds only;
  new `dev-mode` feature, forwarded from the shell through the module.
- OpenHarmony: EntryAbility forwards no launch parameters unless
  build-home-ohos.py --dev-mode stages it (a separate bundle id required).
- Android: Home's `dev-mode` feature comes from build-home.py's
  `--dev-mode` (App Studio's, #477): standalone only, never the installed
  Home's identity under its signer, and ROM staging refuses it. So a build
  that reads launch input is never the installed Home.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Docs and scripts that passed `makepad.*` launch extras to release packages
now name a development test build: `rom/scripts/build-home.py --dev-mode`
under a separate package name on Android, `build-home-ohos.py --dev-mode`
on OpenHarmony, `--features dev-mode` for the desktop mail demo, and
`DEV_MODE=1` / `--features dev-mode` for AppCard's own tools.
`provision-appcard-llm.sh` requires PACKAGE=dev.makepad.octosense.<suffix>.
AGENTS.md states the rule; English and Chinese docs change together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A desktop's environment is the local user's, like a command-line flag, so
`MAKEPAD_APP_CONFIG` (test actions, Mail's demo mailbox, module-open
settings) and AppCard's `MAKEPAD_*` hooks are read in every desktop build.
The gate applies only on Android and OpenHarmony, where a launch Intent extra
or launch parameter can come from another app: there a release build ignores
them and only a development build reads them.

- crates/shell/src/launch_input.rs: `honoured` keys on `cfg(native_mobile)`;
  tests say a phone release build ignores it while a desktop build (any
  profile) and a phone development build read it.
- apps/appcard: `launch_set_in(mobile, development, ...)` — a desktop reads
  the hooks, a phone only in a development build.
- Docs restored to plain desktop `cargo run` commands; the phone and
  OpenHarmony guidance keeps the development-build note. AGENTS.md states the
  desktop/phone split.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Protects a release Home now, without waiting for a makepad sweep: the shell's
pinned makepad gains the Studio/OCTOS_PROXY launch-input gate from
OctoSense-org/makepad#119 as the next stacked runtime patch over the pinned
base, so a shipped Android Home ignores a Studio host or app proxy from an
Intent extra. The OpenHarmony template part of #119 is not carried here, since
Home builds its own OpenHarmony entry ability.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Text editing worked on simple PDFs and broke on real-world ones. On a page a
web browser printed, the engine reads a title drawn in several fonts as one
paragraph per piece; text a paragraph's subset font lacks is set in a
stand-in (Helvetica or Times) that runs over the pieces after it; and
Chinese text is refused with the engine's build note ("set CRAFT_FONTS_DIR
to a craft-fonts checkout").

pdf.edit_check takes pdf.edit_text's arguments and answers {keeps_font,
font, box, covers, off_page}: the font the edit would be drawn in, where the
edited text would be, how many other lines it would newly run into, and
whether it would leave the page. It runs the engine's own editor
(pdfcraft-edit's rewrite_block and replace_line, what text_edit runs) on a
copy opened from the open document's working bytes and drops it: no engine
command, nothing written, the read-only and security checks the engine
makes. pdfcraft-edit joins the pins at the same ymote/pdfcraft revision; it
was already in the graph.

pdf.edit_text and pdf.edit_check now refuse in words for a person, still
invalid: ("PDF Tools can't edit Chinese, Japanese or Korean text yet.";
other scripts name the characters; a locked or read-only PDF; a stale
paragraph number).

pdftools_fixture --edit-sample writes a page as a browser prints it: a title
in five pieces in unnamed Type 3 fonts, and Chinese text in an embedded
CID-keyed TrueType subset (glyphs drawn here as placeholders). The tests use
it to hold the check to what the edit then does (same font, same box), and
show the refusals change nothing. Release timings: a check takes 0.4-0.6 ms
on the samples and 16-19 ms on a 3.4 MB browser-printed page of 399
paragraphs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Edit panel asks the engine what the edit would do (pdf.edit_check) when
a paragraph is clicked and again when typing, the alignment or the colour
pauses, and says it under the Font box: that the paragraph's font can't show
the new text, so it is set in Helvetica, Times or Courier; that the text
needs more room and runs into N lines nearby; that part of it falls off the
page; or, in red, why the engine won't make it, in the service's plain
words. A paragraph that is one piece of a line the PDF stores in pieces says
how many. While there is a warning Apply reads "Apply anyway"; an edit not
checked as it stands is checked first and made at once only if there is
nothing to say. An older engine without the check edits as before.

The Font box names fonts as a person would ("Menlo Regular", not
"ABCDEF+Menlo-Regular"; "Unnamed font" for a browser's Type 3 fonts).

The model tests cover the new pure functions, and now also parse the whole
script, views and handlers included, which the model VM never ran: one
parse error there fails the whole app when it loads. READMEs in English and
Chinese describe Editing text and the --edit-sample page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A hidden desktop shell on a headless weston showed the notes pushing Apply
and Cancel out of the card in the shell's default window (an app area of
1292 x 662). With notes showing, the card now takes the panel's height: the
fields (Font and the notes, Alignment, Colour) scroll, and Apply and Cancel
stay at its foot. Without notes it is design 08's card, as before.

The notes are shorter, the consequence first: "Set in Helvetica: this
paragraph's font can't show all of this text.", "Longer than its box: it
runs into 2 lines of text nearby.", "The PDF stores this line in 5 pieces:
only this one changes."

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A desktop shell built at c50104d, with the real pdf engine, on the Linux
build host's private headless weston (another session was running OctoSense
on the Mac), driven over the remote bridge on the --edit-sample page, light
and dark, at the runtime's 64 ms script budget: the five-pieces note, the
font and room warnings with Apply anyway in view, the plain refusal of
Chinese text inline and on Apply, an edit made anyway (the engine's result
overlaps as the panel said) and one made at once. Frames cropped to the
app's 1292 x 662 area, a receipt, and READMEs in English and Chinese;
README's Status lists it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The coordinator's contract text for #501: pdf.edit_check's arguments
and answer, its plain refusals, and the correction that an edited
paragraph keeps its own font only when that font can show the new text
(otherwise the engine draws a stand-in, which edit_check reports first).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ui.py launched the shell only from a Terminal.app tab. On Linux, where
MAKEPAD_HIDE_WINDOWS does nothing and Xwayland may be missing, each shell
session now gets a headless weston of its own (pixman, never idle, a
private runtime folder) and runs on it with Makepad's Wayland backend, in a
session of its own, with the run home's XDG folders. /quit ends it as
before; weston stops after the shell, and a shell that never answers is
stopped too. Another OctoSense on the machine no longer holds a Linux run
up, since it cannot share the run's compositor.

makepad's Linux file dialog runs the desktop's own helper (zenity, qarma,
matedialog, kdialog). On a host with none, the shell offers no
files.import, so the runs check Home's "Not available here" and the
button's note there instead of the macOS refusal; with a helper on PATH
the run's focused window would open a real dialog, so the step is left
out and run.json says so. macOS runs are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PDF Tools had been verified only on macOS. On the Linux build host
(Ubuntu 26.04, x86_64, software GL), a shell built from main 5cab54a
with the real pdf engine passed ui.py's shell (light and dark), restart,
full and empty runs at the runtime's 64 ms budget three times: in UTC,
in UTC+14 (comment dates across midnight), and in UTC with the final
ui.py. Every home was on ext4.

With a stand-in for the dialog helper first on PATH, "Open a PDF from
this device" imported a PDF for real through makepad's Linux dialog path:
the bytes copied in, titled from the chosen name, opened in a tab. No
Linux-only bug turned up. Frames, Linux-beside-macOS sheets, the receipt
and the findings are in tests/evidence/linux-20261011, in English and
Chinese; the README's status says what is verified and what is not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ymote
ymote force-pushed the test/pdftools-linux branch from 95d8403 to 37008bb Compare October 11, 2026 18:16
@ymote
ymote marked this pull request as ready for review October 11, 2026 18:16
ymote added a commit that referenced this pull request Oct 11, 2026
Local CI passed on the merge train's head 37008bb (#488 → #501 → #495): two runs, the Linux host and the Mac with rom, plus GitHub's embedded-browser acceptance on this head. See the PR comment.
ymote added a commit that referenced this pull request Oct 11, 2026
Local CI passed on the merge train's head 37008bb (#488 → #501 → #495): two runs, the Linux host and the Mac with rom, plus GitHub's embedded-browser acceptance on this head. See the PR comment.
@ymote
ymote merged commit cf06ac5 into main Oct 11, 2026
3 of 11 checks passed
@ymote

ymote commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

Merged in a train (#488 → #501 → #495) on two local runs of the train head 37008bb, which contains this PR's head 37008bb. Merged in order with gh pr merge --admin --merge --match-head-commit; main's tree after the last merge equals the tested tree.

  • Run 1: tools/ci-local.sh --only all --linux-host --offload, PASSED in 22m21s (55 PASS, 8 SKIPPED). On the Linux build host: 53 steps, among them the ubuntu jobs (apps.yml services and kernel-security) and the Linux process sandbox (Landlock, seccomp). Its 8 skips are rom.yml's Linux steps, which need a JDK, the Android SDK and node; run 2 ran them.
  • Run 2: tools/ci-local.sh --only all on the Mac with JDK 17, Android SDK 35 and node 24, PASSED in 23m32s (59 PASS), nothing skipped. The train touches rom/ (Read launch-time test hooks only in development builds #488), so the two-run rule applies to it.
Run 2's table
Job Step Result Time
ci-local / drift The local mapping fits the workflows PASS -
desktop.yml / desktop Prepare the pinned framework sources PASS 1s
desktop.yml / desktop Compile the desktop with the shared runtime PASS 3m15s
desktop.yml / desktop One copy of every shell source PASS -
desktop.yml / desktop Desktop tools and scripts PASS 14s
desktop.yml / desktop In-app updater and installer handoff PASS 4s
desktop.yml / desktop One Makepad, App Hub, octos and Rinx in the locked graph PASS 5s
desktop.yml / desktop The native apps' generated places match native-apps.json PASS 0s
desktop.yml / native-host-api Prepare the pinned framework sources PASS 0s
desktop.yml / native-host-api Build the native acceptance host and pinned Hub CLI PASS 9s
desktop.yml / native-host-api Signed app tool calls a real OS permission API PASS 3s
phone.yml / home Prepare pinned sources and the reviewed runtime patch PASS 0s
phone.yml / home Validate launcher icons and Apple asset catalogs PASS 3s
phone.yml / home Compile Home and its bundled modules PASS 3m17s
phone.yml / home Verify the single runtime graph PASS 3s
phone.yml / home The native apps' generated places match native-apps.json PASS 0s
phone.yml / home The pinned octos revision PASS -
phone.yml / home Build octos (unless cached) PASS -
phone.yml / home Build Home, App Hub admission and runtime policy tests PASS 43s
phone.yml / home Test Home, App Hub admission and runtime policy PASS 40s
phone.yml / home Two-lane scenario (real kernel, scripted model) PASS 31s
phone.yml / home Build opt-in AppCard tests PASS 34s
phone.yml / home Test opt-in AppCard PASS 6s
phone.yml / home Build craft-engine relay and system-chat tests PASS 39s
phone.yml / home Test craft-engine relay and system chat PASS 50s
phone.yml / home Build linked Terminal tests PASS 32s
phone.yml / home Test linked Terminal PASS 6s
phone.yml / home Build Wasm and runtime policy tests PASS 8s
phone.yml / home Test Wasm and runtime policy PASS 16s
apps.yml / services python3 tools/setup.py PASS 1s
apps.yml / services Shared system app interface PASS -
apps.yml / services Kernel service PASS 21s
apps.yml / services OctoSense release updater PASS 6s
apps.yml / services App peers PASS 26s
apps.yml / services AI providers config PASS 10s
apps.yml / services Connected provider protocols and Linux host adapter PASS 32s
apps.yml / services L0 in-card chat (sys.chat, host side) PASS 12s
apps.yml / services System toolbox (workflow templates) PASS 1m57s
apps.yml / kernel-security python3 tools/setup.py PASS 1s
apps.yml / kernel-security The pinned octos revision PASS 0s
apps.yml / kernel-security Build octos (unless cached) PASS -
apps.yml / kernel-security Real-kernel tests PASS 30s
apps.yml / apps python3 tools/setup.py PASS 1s
apps.yml / apps Host services (Mail, AI providers, News) PASS 1m12s
apps.yml / apps Connected accounts and Rinx editor PASS 1m14s
apps.yml / apps Shell AI services (crates/ai-host) PASS 2m29s
apps.yml / apps AppCard runtime setup safeguards PASS 0s
apps.yml / apps AppCard clippy (own crates; shared runtime compiles) PASS 52s
apps.yml / apps AppCard transport and store PASS 15s
apps.yml / apps One Makepad, App Hub, octos and Rinx in the locked graph PASS 4s
rom.yml / product python3 -m unittest discover -s tests -v PASS 38s
rom.yml / product Check generated Agent Binder client PASS 0s
rom.yml / product bash -n scripts/stage-forks.sh scripts/publish-release.sh scripts/apply-to-tree.sh scripts/build-rom.sh PASS -
rom.yml / web-installer npm ci --ignore-scripts --no-audit --no-fund PASS 7s
rom.yml / web-installer npm test PASS 1s
rom.yml / web-installer npx playwright install --with-deps chromium PASS 1s
rom.yml / web-installer npm run test:browser PASS 5s
rom.yml / web-installer python3 -m unittest discover -s tests -p test_installer_manifest.py -v PASS 0s
rom.yml / web-installer bash -n scripts/release.sh PASS -

Notes:

  • GitHub runs Python 3.12; this run used Python 3.9.6
  • apps.yml:services runs on ubuntu-latest in GitHub; here on macOS, so #[cfg(target_os = "linux")] code in it is not exercised (--linux-host runs it on Linux)
  • apps.yml:kernel-security runs on ubuntu-latest in GitHub; here on macOS, so #[cfg(target_os = "linux")] code in it is not exercised (--linux-host runs it on Linux)

Run 2's first attempt on the same head stopped at one failure outside this train: crates/wasm-host/tests/runtime.rs an_endless_loop_stops_at_its_deadline_and_spends_the_instance measured 189.8 ms against its 190 ms floor (its deadline fired a tick early). The rerun passed every step.

GitHub's native Windows acceptance (embedded-browser.yml, which local CI cannot replace) passed on this head: run 38163147170. Main after the train: cf06ac5, whose tree equals the tested tree.

@ymote
ymote deleted the test/pdftools-linux branch October 11, 2026 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant