DEV Community

Cover image for Termux Isn't Linux. I Packaged 70+ Tools for It Anyway
Gouranga Das Samrat
Gouranga Das Samrat

Posted on

Termux Isn't Linux. I Packaged 70+ Tools for It Anyway

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
Enter fullscreen mode Exit fullscreen mode

Three consequences, and every build failure I've fixed traces back to one of them:

  1. No glibc. Bionic is smaller and has gaps. It has no shm_open, for example, and a lot of upstream code never checks.
  2. No standard filesystem layout. Everything lives under $PREFIX. You can't create /tmp or /bin/sh without root, and I'm not interested in solutions that need root.
  3. 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 with SIGSYS.

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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, iredis and mongosh
  • drive cloud accounts with azure-cli, oci-cli and doctl
  • lint and secure Go code with golangci-lint, gosec and govulncheck
  • cut releases with goreleaser and git-cliff
  • load test an API with k6, oha or hey
  • use jujutsu instead 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)