Termux looks like Linux right up until you try to build something that matters.
You get a shell, a package manager, a familiar prompt. Then you clone a project, run its build script, and watch it die on a path that doesn't exist, a syscall you aren't allowed to make, or a libc function that was never there.
I got tired of watching it fail, so I started fixing it. I now maintain 70+ packages for Termux, including things people assume can't run on a phone: Bun, Zed, Ghostty, Playwright, Codex, DBeaver. This is the first of two posts. This one is about the wall. The next one is about what's on the other side.
What Termux actually is
Termux is not a Linux distro that happens to run on Android. It's a userland compiled against Android's own C library, Bionic, living inside a private app directory.
$ echo $PREFIX
/data/data/com.termux/files/usr
$ ls /bin /tmp /usr
ls: cannot access '/bin': No such file or directory
ls: cannot access '/tmp': No such file or directory
ls: cannot access '/usr': No such file or directory
Three consequences, and every build failure I've fixed traces back to one of them:
-
No glibc. Bionic is smaller and has gaps. It has no
shm_open, for example, and a lot of upstream code never checks. -
No standard filesystem layout. Everything lives under
$PREFIX. You can't create/tmpor/bin/shwithout root, and I'm not interested in solutions that need root. -
A syscall filter. Android sandboxes apps with seccomp. Depending on the Android version, calling a filtered syscall doesn't return
ENOSYS. It kills the process withSIGSYS.
There is a route where you drag glibc along with a runner and execute stock Linux binaries. It works for some things. I'd rather ship native packages, so the person on the other end types pkg install and moves on with their day.
Fix 1: rewrite the paths, don't emulate them
Upstream code hardcodes /usr/share/whatever and /etc/whatever. Fighting that at runtime is a losing game. Patch it at build time, once:
termux_step_pre_configure() {
sed -i "s|\"/usr/share/example-tool\"|\"$TERMUX_PREFIX/share/example-tool\"|" \
src/paths.c
}
When the same placeholder shows up in several files, I use a token like @TERMUX_PREFIX@ in a patch and substitute it in the recipe, so the patch stays readable and the real path gets injected at build time.
Shebangs are the same problem in a different costume. #!/bin/bash and #!/usr/bin/env python point at nothing:
termux-fix-shebang script.sh
head -1 script.sh
# #!/data/data/com.termux/files/usr/bin/bash
termux-exec papers over some of this at runtime, but it only covers exec. It does nothing when a program calls open("/tmp/foo").
Fix 2: hardcoded temp dirs
/tmp is the most common offender. Bun needed a patch for it. Ghostty needed one. The shape is always the same:
- const char *dir = "/tmp";
+ const char *dir = getenv("TMPDIR");
+ if (!dir || !*dir) dir = "@TERMUX_PREFIX@/tmp";
Tiny diff. Takes an hour to find, because the failure shows up three layers away from the cause, usually as "permission denied" on a path you didn't know the program was using.
Fix 3: syscalls, or how to get killed by a feature probe
This is the nasty one. Modern runtimes try fast paths: close_range, pidfd_open, statx, io_uring. On a normal Linux box, an old kernel answers ENOSYS and the program falls back to the slow path. On Android, the filter can answer with SIGSYS and your build dies with "Bad system call".
Bun needed three separate patches for this: close_range, pidfd_open, and statx. Ghostty needed two: one to skip an io_uring probe, and one to replace its pidfd-based process watcher.
The fix is almost boring:
// illustrative shape, not a verbatim patch
- if (close_range(3, ~0U, 0) == 0) return;
+#ifndef __ANDROID__
+ if (close_range(3, ~0U, 0) == 0) return;
+#endif
+ close_fds_slowly(3);
Upstream already maintains the fallback because it supports old kernels. All I do is make Android look like an old kernel. Compile the fast path out, let the slow path run, move on.
Fix 4: let the toolchain do the cross-compiling
Most of my packages are Go and Rust, and that's not an accident. Both cross-compile cleanly, and Termux's build system ships helpers for them. A Go CLI recipe is nearly trivial:
TERMUX_PKG_HOMEPAGE=https://github.com/example/example-tool
TERMUX_PKG_DESCRIPTION="Example Go CLI"
TERMUX_PKG_LICENSE="MIT"
TERMUX_PKG_MAINTAINER="@GourangaDasSamrat"
TERMUX_PKG_VERSION=1.2.3
TERMUX_PKG_SRCURL=https://github.com/example/example-tool/archive/v$TERMUX_PKG_VERSION.tar.gz
TERMUX_PKG_SHA256=<sha256>
TERMUX_PKG_BUILD_IN_SRC=true
termux_step_make() {
termux_setup_golang
go build -trimpath -o example-tool ./cmd/example-tool
}
termux_step_make_install() {
install -Dm755 example-tool "$TERMUX_PREFIX/bin/example-tool"
}
Rust is the same idea with termux_setup_rust, plus a pile of crate-specific pain I'll get to in part two.
Python packages with native extensions are their own category. pip install on a phone means compiling Rust or C on a phone, which is a bad time. That's why things like python-rpds-py, python-pynacl, python-crc32c and python-jellyfish exist as packages: one build on my side, an instant install on yours.
What this buys you
Once the wall is down, a phone stops being a toy terminal. From Termux you can now:
- talk to databases with
pgcli,mycli,litecli,iredisandmongosh - drive cloud accounts with
azure-cli,oci-clianddoctl - lint and secure Go code with
golangci-lint,gosecandgovulncheck - cut releases with
goreleaserandgit-cliff - load test an API with
k6,ohaorhey - use
jujutsuinstead of git, if you're that kind of person
None of those work out of the box. Each one has a recipe, and some have a pile of patches behind them.
Why I bother
Because "Termux can't do that" is almost never true. It's usually "nobody has patched /tmp yet."
If you want to see the full list of what I maintain, it's on Repology. The recipes and patches live in the Termux packages repo, and my other work is on GitHub.
Part two gets into the hard cases: a JavaScript runtime with nine patches, a GPU-accelerated terminal, and a code editor whose dependency tree needed surgery.
If you've hit a Bad system call in Termux and couldn't figure out why, drop it in the comments. I've probably seen it.
Top comments (0)