Building FAPs on Android with Termux

July 31, 2026 · View on GitHub

Contributed by @CamsShaft, adapted from BUILD-FLIPPER-FAPS-IN-TERMUX (MIT).

Set up ufbt (micro Flipper Build Tool) inside Termux on a stock Android phone and compile Flipper Zero applications (.fap) on-device. No PC, no proot, no virtual machine.

Scope: this builds applications, not the firmware. Building the firmware itself with fbt is not covered here — see HowToBuild.md.

Originally worked out on a Samsung Galaxy S22 (SM-S901W, aarch64). The paths below assume an aarch64 device and the default Termux prefix ($PREFIX = /data/data/com.termux/files/usr). Nothing here is guaranteed to match your device exactly, but the failure modes are documented in Troubleshooting.

Why this needs extra work

ufbt downloads a prebuilt arm-none-eabi-gcc cross-toolchain built for ordinary Linux against glibc. Termux runs on Android's Bionic libc with a non-standard filesystem layout, and every ELF binary in that toolchain asks for /lib/ld-linux-aarch64.so.1 as its dynamic linker — which does not exist on Android.

The fix is to install Termux's glibc compatibility layer, then run each toolchain binary through glibc's own linker via a small wrapper script.

Do not run patchelf on the toolchain binaries. It corrupts their ELF segment layout and you get segfaults out of cc1. The wrappers below leave the binaries byte-for-byte untouched.

Prerequisites

  • An aarch64 Android device
  • Termux installed from F-Droid — the Google Play build is obsolete and will not work
  • An internet connection and a couple of GB free for the toolchain, SDK and glibc

1. Base packages

pkg update && pkg upgrade -y
pkg install python python-pip git file -y

file is not optional — the wrapper loop in step 6 uses it to tell ELF binaries apart from everything else.

2. glibc compatibility layer

pkg install glibc-repo -y
pkg install glibc-runner -y

These have to be two separate commands: glibc-repo is what adds the glibc repository that glibc-runner is served from. Together they install a full glibc under $PREFIX/glibc/, including the dynamic linker we need. Verify it:

ls $PREFIX/glibc/lib/ld-linux-aarch64.so.1

Never set LD_PRELOAD to glibc's libc.so.6 globally. It poisons Termux's own Bionic binaries, Python included, and the breakage is not obvious. The wrappers in step 6 remove any need for it.

3. Install ufbt and the Python build dependencies

pip install ufbt colorlog heatshrink2 pyelftools scons pillow
PackageNeeded for
colorlogbuild script logging
heatshrink2icon and asset compression
pyelftoolsthe FASTFAP post-processing step
sconsthe build system — needed as a standalone install, the toolchain's bundled copy is not usable here
pillowicon conversion; without it the only error you get is a bare convert: No such file or directory

If the pillow wheel fails to build, use the Termux package instead: pkg install python-pillow.

4. Point ufbt at the Unleashed SDK

ufbt defaults to the official firmware SDK. To build against Unleashed, pass this repository's update index:

ufbt update --index-url=https://up.unleashedflip.com/directory.json --channel=dev

Channels are dev and release. The index URL is also listed in the ReadMe. Every later ufbt update needs the same flags, otherwise you silently fall back to the official SDK and your app is built against the wrong API.

If ~/.ufbt/toolchain/ is still empty after that, any ufbt invocation bootstraps the toolchain:

ufbt -h

An error mentioning python3 at this point is expected — step 5 fixes it. Check that the toolchain landed:

ls ~/.ufbt/toolchain/aarch64-linux/bin/arm-none-eabi-gcc
ls ~/.ufbt/toolchain/aarch64-linux/arm-none-eabi/bin/as
ls ~/.ufbt/toolchain/aarch64-linux/libexec/gcc/arm-none-eabi/*/cc1

5. Repoint the toolchain's Python

The toolchain ships its own glibc-linked Python, which cannot run here. Point every python3* entry at Termux's Python instead:

TOOLCHAIN="$HOME/.ufbt/toolchain/aarch64-linux"

for py in "$TOOLCHAIN"/bin/python3*; do
    [ -e "$py" ] || [ -L "$py" ] || continue
    ln -sf "$PREFIX/bin/python3" "$py"
done

6. Wrap the toolchain binaries

This is the step everything else depends on. Each wrapper invokes the real binary through glibc's dynamic linker with an explicit --library-path, so the binary itself is never modified.

TOOLCHAIN="$HOME/.ufbt/toolchain/aarch64-linux"
GLIBC_LIB="$PREFIX/glibc/lib"

wrap() {
    for f in "$@"; do
        [ -f "$f" ] || continue
        case "$f" in *.real | *.so | *.so.*) continue ;; esac
        [ -e "$f.real" ] && continue
        file -b "$f" | grep -q '^ELF' || continue

        mv "$f" "$f.real"
        cat > "$f" <<WRAPPER
#!$PREFIX/bin/bash
exec "$GLIBC_LIB/ld-linux-aarch64.so.1" --library-path "$GLIBC_LIB:$TOOLCHAIN/lib" "$f.real" "\$@"
WRAPPER
        chmod +x "$f"
        echo "wrapped $f"
    done
}

wrap "$TOOLCHAIN"/bin/arm-none-eabi-*
wrap "$TOOLCHAIN"/arm-none-eabi/bin/*
wrap "$TOOLCHAIN"/libexec/gcc/arm-none-eabi/*/*

Those three globs are three separate directories of ELF binaries. Missing one gives a different failure:

DirectoryContainsError if not wrapped
bin/arm-none-eabi-gcc, -g++, ...cannot execute / No such file or directory
arm-none-eabi/bin/as, ld, ar, objcopy, ...cannot execute as
libexec/gcc/arm-none-eabi/<gcc-version>/cc1, cc1plus, collect2, lto1cannot execute cc1

The guards matter, especially if you re-run this later:

  • *.so is skipped because liblto_plugin.so lives in libexec/ and is a shared library, not an executable. Wrapping it gives invalid ELF header.
  • *.real and the $f.real check keep the function idempotent, so re-running it after a toolchain update cannot wrap a wrapper.
  • In bin/ only arm-none-eabi-* is wrapped, deliberately: bin/python3 now points at Termux's Bionic Python and must not go through glibc's linker.

ufbt update can replace the toolchain — re-run steps 5 and 6 after every update.

7. Build

git clone https://github.com/<owner>/<some-flipper-app>.git
cd <some-flipper-app>
ufbt

A successful build ends like this:

scons: Entering directory '...'
        CC      ...
        LINK    ...
        FAP     ...
        FASTFAP ...
        APPCHK  dist/some_app.fap
                Target: 7, API: 88.2

The .fap is in dist/. Check the reported API against the firmware on your Flipper — a mismatch means the app will refuse to load.

8. Getting the .fap onto the Flipper

ufbt launch and ufbt flash_usb need a serial connection to the Flipper (/dev/ttyACM*). On a stock, non-rooted Android this does not work — Android does not expose USB CDC-ACM device nodes to Termux, so there is nothing for ufbt to talk to. Only a rooted device with a working CDC-ACM driver can use those commands.

Copy the file out instead. FAPs live in /ext/apps/<Category>/ on the microSD card (see AppsOnSDCard.md):

termux-setup-storage                        # one-time: grants Termux access to shared storage
cp dist/some_app.fap ~/storage/downloads/

From Downloads the file is visible to every other app on the phone, so you can move it to the Flipper the way you normally would — the Flipper Mobile App, or a USB-C microSD card reader.

Troubleshooting

cannot execute cc1 / cannot execute as

One of the three directories in step 6 was not wrapped. Re-run the wrap calls — they are safe to repeat.

liblto_plugin.so: invalid ELF header

A shared library got wrapped. Restore it:

LIBEXEC="$(echo "$HOME"/.ufbt/toolchain/aarch64-linux/libexec/gcc/arm-none-eabi/*)"
mv "$LIBEXEC/liblto_plugin.so.real" "$LIBEXEC/liblto_plugin.so"

Segmentation fault from cc1

The binary was corrupted, almost always by patchelf. Re-download the toolchain and re-apply the wrappers:

rm -rf ~/.ufbt/toolchain
ufbt -h

Then redo steps 5 and 6. Never use patchelf on these binaries.

No module named 'colorlog' / 'heatshrink2' / 'elftools'

pip install colorlog heatshrink2 pyelftools

convert: No such file or directory

pillow is missing: pip install pillow, or pkg install python-pillow.

python3: No such file or directory

The toolchain's Python symlink is broken or was restored by an update. Re-run step 5.

LD_PRELOAD is pointed at glibc's libc.so.6, which breaks Termux's own Bionic binaries. Clear it:

unset LD_PRELOAD
unset -f ufbt          # in case a shell function is shadowing ufbt

Then check your shell startup files for leftovers:

grep -r "glibc\|LD_PRELOAD" ~/.bashrc ~/.profile ~/.bash_profile ~/.zshrc 2>/dev/null

ufbt update says the SDK is up to date, but the toolchain is missing

ufbt tracks the SDK and the toolchain separately. Force both:

rm -rf ~/.ufbt/current ~/.ufbt/toolchain
ufbt update --index-url=https://up.unleashedflip.com/directory.json --channel=dev
ufbt -h

Then redo steps 5 and 6.

The app builds but reports the wrong API version

An ufbt update was run without --index-url, so ufbt switched back to the official SDK. Re-run the command from step 4.

The wrappers disappeared after an update

Expected — ufbt update can replace the whole toolchain directory. Re-run steps 5 and 6.

What actually happens

Each wrapper invokes its real binary through glibc's dynamic linker with an explicit --library-path pointing at glibc's libraries. When arm-none-eabi-gcc execs cc1, it hits a shell script that chains through the linker to the untouched .real binary. The cross-compiled output is unaffected — it targets bare-metal Cortex-M, not Linux.

ufbt (Python, Bionic)
  └─ arm-none-eabi-gcc (wrapper)
       └─ ld-linux-aarch64.so.1 → arm-none-eabi-gcc.real (glibc)
            ├─ cc1 (wrapper)      → ld-linux-aarch64.so.1 → cc1.real
            ├─ as (wrapper)       → ld-linux-aarch64.so.1 → as.real
            └─ collect2 → ld (wrapper) → ld-linux-aarch64.so.1 → ld.real
                 └─ output: some_app.fap (ARM Cortex-M4, for the Flipper Zero)

Credits

Worked out and written up by @CamsShaft through extensive trial and error on a Samsung Galaxy S22 running Termux — no PC, no proot, no virtual machines. Original guide and its MIT license: BUILD-FLIPPER-FAPS-IN-TERMUX.