Skip to content

Instantly share code, notes, and snippets.

@Jobians
Created August 2, 2026 23:30
Show Gist options
  • Select an option

  • Save Jobians/207eeb6d09298e28a5f2aa0175f2af49 to your computer and use it in GitHub Desktop.

Select an option

Save Jobians/207eeb6d09298e28a5f2aa0175f2af49 to your computer and use it in GitHub Desktop.

Bundling proot (or any Termux binary) inside an Android app

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.

Why this works

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:

  1. Getting the files into a location Android will grant execute permission on.
  2. Fixing up the dynamic linker's expected filenames so everything resolves.

Step 1 — Get the binary and its shared libs

If you have Termux installed on the same device:

pkg install <package-name>
dpkg -L <package-name>   # lists every file the package installs

Binaries 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.)

Step 2 — Place files where Android will let you exec them

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 lib prefix and .so suffix:
    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.

Step 3 — Force native lib extraction

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.

Step 4 — Patch the linker's expected names

Renaming libtalloc.so.2libtalloc.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   # verify

Step 5 — Walk the dependency tree

Repeat for every non-system dependency:

patchelf --print-needed libproot.so

Anything 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.solibtalloc.so + libandroid-shmem.so → (liblog.so, libc.so — both system libs, done).

Step 6 — Rebuild and reinstall

./gradlew clean
./gradlew installDebug

Step 7 — Invoke it from Kotlin

val 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.

Known limitations

  • 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment