Files
7f12ed3976 docs: building FAPs on Android with Termux (no PC) (#1060)
* docs: add "Building FAPs on Android with Termux" guide

Documents how to run ufbt inside Termux on a stock Android phone and
build .fap applications on-device - no PC, proot or VM. Adapted with
permission from @CamsShaft's BUILD-FLIPPER-FAPS-IN-TERMUX (MIT), see
issue #1028, with these changes for this repo:

- Point ufbt at the Unleashed update index instead of the official SDK,
  and warn that a plain `ufbt update` silently switches back to it.
- Make the toolchain wrapper loop idempotent and version-agnostic:
  glob the gcc version directory instead of hardcoding 12.3.1, skip
  already-wrapped .real payloads so re-running after a toolchain update
  can't wrap a wrapper, and keep bin/python3 out of it (it points at
  Termux's Bionic Python and must not go through glibc's linker).
- Replace the `ufbt launch` deploy step: Android doesn't expose USB
  CDC-ACM device nodes to Termux, so it can't work unrooted. Copy the
  .fap out via shared storage instead.

Linked from HowToBuild.md and the ReadMe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* upd changelog: FAPs on Android with Termux doc

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 00:07:19 +03:00

309 lines
11 KiB
Markdown

# Building FAPs on Android with Termux
> Contributed by **[@CamsShaft](https://github.com/CamsShaft)**, adapted from
> [BUILD-FLIPPER-FAPS-IN-TERMUX](https://github.com/CamsShaft/BUILD-FLIPPER-FAPS-IN-TERMUX) (MIT).
Set up `ufbt` (micro Flipper Build Tool) inside [Termux](https://termux.dev/) 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](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](#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](https://f-droid.org/en/packages/com.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
```sh
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
```sh
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:
```sh
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
```sh
pip install ufbt colorlog heatshrink2 pyelftools scons pillow
```
| Package | Needed for |
|---|---|
| `colorlog` | build script logging |
| `heatshrink2` | icon and asset compression |
| `pyelftools` | the `FASTFAP` post-processing step |
| `scons` | the build system — needed as a standalone install, the toolchain's bundled copy is not usable here |
| `pillow` | icon 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:
```sh
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](../ReadMe.md#-links).
**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:
```sh
ufbt -h
```
An error mentioning `python3` at this point is expected — step 5 fixes it. Check that the toolchain landed:
```sh
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:
```sh
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.
```sh
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:
| Directory | Contains | Error 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`, `lto1` | `cannot 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
```sh
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](AppsOnSDCard.md)):
```sh
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:
```sh
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:
```sh
rm -rf ~/.ufbt/toolchain
ufbt -h
```
Then redo steps 5 and 6. **Never use `patchelf` on these binaries.**
### `No module named 'colorlog'` / `'heatshrink2'` / `'elftools'`
```sh
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.
### `CANNOT LINK EXECUTABLE "python3": library "ld-linux-aarch64.so.1" not found`
`LD_PRELOAD` is pointed at glibc's `libc.so.6`, which breaks Termux's own Bionic binaries. Clear it:
```sh
unset LD_PRELOAD
unset -f ufbt # in case a shell function is shadowing ufbt
```
Then check your shell startup files for leftovers:
```sh
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:
```sh
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](#4-point-ufbt-at-the-unleashed-sdk).
### 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](https://github.com/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](https://github.com/CamsShaft/BUILD-FLIPPER-FAPS-IN-TERMUX).