This is a recipe for taking a binary/library built for Termux and getting it running
inside your own Android app via ProcessBuilder, without root.
Termux builds its packages against Bionic libc using the Android NDK, so the resulting binaries are already ABI-compatible with plain Android apps. The trick is just:
- Getting the files into a location Android will grant execute permission on.
- Fixing up the dynamic linker's expected filenames so everything resolves.
If you have Termux installed on the same device:
pkg install <package-name>
dpkg -L <package-name> # lists every file the package installsBinaries are typically under usr/bin/, shared libraries under usr/lib/.
(Alternatively, Termux publishes .deb packages at
https://packages-cf.termux.dev/apt/termux-main/pool/main/ — you can pull these
directly on a dev machine with wget, then extract with ar x + tar xf without
needing Termux installed at all.)
Android 10+ enforces W^X: you can't exec() a file sitting in normal writable storage.
The one place guaranteed to have exec permission is your app's nativeLibraryDir,
which is populated automatically from jniLibs/ at build time — but only for files
matching lib*.so.
So:
- Executables get renamed with a
libprefix and.sosuffix:cp usr/bin/proot app/src/main/jniLibs/arm64-v8a/libproot.so
- Shared libraries get their version suffix stripped:
cp usr/lib/libtalloc.so.2 app/src/main/jniLibs/arm64-v8a/libtalloc.so
Use cp -L if the source is a symlink, so you copy the real file, not a dangling link.
Modern Android Gradle Plugin defaults to keeping native libs compressed inside the APK
(extractNativeLibs=false), which works for dlopen()-loaded libraries but not
for anything you plan to exec() via ProcessBuilder. Force real on-disk extraction:
// app/build.gradle.kts
android {
packaging {
jniLibs {
useLegacyPackaging = true
}
}
}Or via manifest (older AGP):
<application android:extractNativeLibs="true" ... />Clean-build after this change — it won't retroactively fix an already-built APK.
Renaming libtalloc.so.2 → libtalloc.so doesn't change what the consuming binary
(libproot.so) has hardcoded as its dependency (DT_NEEDED entry). Fix it with
patchelf (install via pkg install patchelf in Termux):
patchelf --replace-needed libtalloc.so.2 libtalloc.so libproot.so
patchelf --print-needed libproot.so # verifyRepeat for every non-system dependency:
patchelf --print-needed libproot.soAnything that isn't a standard Android system library (libc.so, libm.so,
libdl.so, liblog.so, libz.so, etc. — always present on-device) needs to be
copied into jniLibs/<ABI>/ and patched the same way. Keep walking --print-needed
on each new file you add until every non-system .so is accounted for.
For proot specifically, the chain is: libproot.so → libtalloc.so +
libandroid-shmem.so → (liblog.so, libc.so — both system libs, done).
./gradlew clean
./gradlew installDebugval nativeLibDir = applicationInfo.nativeLibraryDir
val prootPath = "$nativeLibDir/libproot.so"
val processBuilder = ProcessBuilder(prootPath, "--version")
processBuilder.redirectErrorStream(true)
processBuilder.environment()["LD_LIBRARY_PATH"] = nativeLibDir // so it finds bundled deps
val process = processBuilder.start()
val output = process.inputStream.bufferedReader().readText()
process.waitFor()Run this off the main thread — Process calls block.
- Some OEM kernels restrict
ptrace, which proot depends on — test on real devices. - This won't work for packages that need real root, kernel modules, or have
Termux-specific paths (like
$PREFIX) hardcoded into their configs. - Licensing: check the license of whatever you bundle. proot and talloc are GPL/LGPL — redistributing them in a closed-source app may carry obligations (source availability, attribution) depending on your distribution model.