The hard tier is software that assumes a full desktop Linux box. GPU loaders, shared memory, process watchers, JIT engines, and dependency trees so big nobody on the upstream team has read all of them.
I maintain a good chunk of that tier for Termux. Here's what the patches look like, and what each one is really saying.
Bun: nine patches, three of them syscalls
Bun has the longest patch stack I deal with regularly. The filenames tell most of the story:
| Patch | What it fixes |
|---|---|
0001 default to symlink install backend |
bun install uses symlinks by default |
0002 remove Werror and unsupported flags |
Build stops failing on warnings and flags this toolchain lacks |
0003 disable close_range |
Skip a syscall Android may refuse |
0004 set termux tmpdir |
Temp files go to $PREFIX/tmp instead of /tmp
|
0005 fix bash completion empty regex |
Completion generation no longer chokes |
0006 fix symlink bin chmod |
chmod on symlinked binaries stops failing |
0007 disable pidfd_open |
Another filtered syscall, another fallback |
0008 disable statx |
Same story |
0009 android link rust as machine code |
Linking fix for the Rust-built part on Android |
Read that list again. Three of nine are the same bug wearing different hats: the runtime probes a modern kernel feature, Android's filter objects. Upstream already carries fallbacks for old kernels, so the patch is mostly "pretend you're on an old kernel."
I like this class of fix because it's small and honest. It doesn't fork the project. It doesn't rewrite anything. It switches off a fast path and lets the existing slow path run.
Ghostty: GPU terminal, six patches
Ghostty is a good example of a project that makes zero Android assumptions, which is fair, it's a desktop terminal. Six patches got it running:
| Patch | Problem it solves |
|---|---|
0001 dcimgui use desktop GL loader on android |
The UI layer picks the wrong GL loader for the target |
0002 kitty graphics no shm_open on android |
Bionic has no shm_open, so the image protocol needs another path |
0003 global skip xev io_uring probe on android |
The io_uring feature probe is a SIGSYS waiting to happen |
0004 android default shell, home and /bin/sh |
Sensible defaults when there's no /bin/sh and no standard home |
0005 os file android tmpdir |
Yes, /tmp again |
0006 termio android process watcher without pidfd |
Watch child processes without pidfd
|
If you've read part one, you've already seen four of those problems. Only the GL loader is new. The same short list of Android quirks keeps showing up in completely unrelated codebases, and that's the main thing I've learned. After the first dozen ports, a failure stops being a mystery and becomes a checklist.
Zed: when the dependency tree is the problem
Zed is where the approach changes. The editor itself is fine. The trouble is in the crates underneath it. My recipe carries patches for alacritty_terminal, ipc-channel, trash, wasmtime-internal-jit-icache-coherence, wayland-cursor, x11rb-protocol and zed-scap, plus a workspace-level patch that wires them together.
Patching code that lives in other people's crates is where Rust packaging gets fiddly. The workable approach is to vendor the dependencies, patch the vendored sources, and tell Cargo to build from them:
# illustrative shape; real recipes differ per package
termux_step_pre_configure() {
termux_setup_rust
cd "$TERMUX_PKG_SRCDIR"
cargo vendor --locked vendor > /dev/null
mkdir -p .cargo
cat >> .cargo/config.toml <<-EOF
[source.crates-io]
replace-with = "vendored-sources"
[source.vendored-sources]
directory = "vendor"
EOF
for diff in "$TERMUX_PKG_BUILDER_DIR"/*.vendor.diff; do
patch -p1 < "$diff"
done
# Cargo verifies vendored files against recorded checksums.
# Patched files fail that check, so clear the file list.
for f in vendor/*/.cargo-checksum.json; do
sed -i 's/"files":{[^}]*}/"files":{}/' "$f"
done
}
The checksum step is the one that eats an afternoon. You patch a vendored crate, run the build, and Cargo tells you a file changed. It's correct, you did change it. Emptying the files map in .cargo-checksum.json is how you tell it that's intentional.
Zed also ships a zed-shim.sh. I use shims when an app needs environment set up before it starts, and I'd rather wrap it than fork it:
#!@TERMUX_PREFIX@/bin/sh
# illustrative shape
export TMPDIR="${TMPDIR:-@TERMUX_PREFIX@/tmp}"
exec "@TERMUX_PREFIX@/opt/example/bin/example" "$@"
Bruno has the same kind of shim, alongside two patches of its own: one to lazy-load node-pty, and one so the machine-id lookup doesn't try to execute anything.
Browser automation on a phone
Playwright is the one that makes people do a double take. Browser automation needs a browser, a driver, and a runtime that agree with each other. The recipe has android-support.patch, two Firefox patches (firefox-support and firefox-single-window), and a path-matching fix. The Python side, python-playwright, has a use-system-driver.patch, so it talks to the driver installed on the system instead of the one bundled in the wheel.
That bundled driver is a good example of a general rule: anything that ships its own prebuilt binary is a candidate for "swap it for the system one."
The small stuff matters too
Not every patch is dramatic. Some of my favorites are tiny:
-
numbatdisables Android plotting support, because the plotting backend has no business running here. -
pnpmbuildssha2without asm. -
uutils-coreutilscarries fixes for 32-bit devices: anauxvfix, a 32-bitstatmode fix intail, and a smallsyncfs.c. A lot of phones are still 32-bit, and nobody tests for them. -
thonnyneeded four patches aroundmypy, shell detection and path opening. -
alberthas six patches for its XDG handling, system utilities and plugin loading.
What I'd tell anyone starting
Read the failure literally. ENOENT is a path problem. SIGSYS is a syscall problem. A missing symbol is a Bionic gap. Most of the work is classifying the failure correctly.
Prefer small, guarded patches. Wrap the change in __ANDROID__ or the language equivalent, and keep it narrow. Those are patches upstream might actually accept.
Shim before you fork. An env-setting wrapper script is easier to maintain than a diverged repository.
And be honest about the cost. Every version bump is a rebase. Nine patches on Bun means nine things that can break on the next release. That's the part nobody writes blog posts about, and it's most of the job.
Where to find it
Everything I maintain is tracked on Repology, and I'm on GitHub if you want to see the work, open an issue, or tell me which package you'd like to see next.
Termux has limits. I just don't agree with where most people think they are.
Top comments (0)