Reproducible Docker-based cross-build environment for running CPython in the plain Android shell, with no Termux, no APK, and no Gradle. The primary target is Android x86_64 running inside Redroid, while the repository is structured to also build arm64-v8a, armeabi-v7a, and x86.
- A Dockerized build environment for CPython on Android
- Cross-compilation with the Android NDK
- Portable runtime bundles for Android shell usage
full,minimal, andslimruntime variants- Automated Redroid deploy and smoke-test scripts
- Global wrappers in
/system/bin/python3,/system/bin/python, and/system/bin/pip - Bundled pure-Python packages:
fakerrequestsjinja2
- CPython:
3.12.12 - Android NDK:
r27d - Android API baseline:
24 - Primary tested ABI:
x86_64 - Additional supported build targets:
arm64-v8aarmeabi-v7ax86
The GitHub release workflow currently publishes prebuilt assets for:
x86_64arm64-v8a
The runtime uses shell launchers to set:
PYTHONHOMEPYTHONPATHLD_LIBRARY_PATHLD_PRELOADTMPDIRLANGLC_ALLSSL_CERT_FILE
This is more reliable on Android than depending on linker search behavior alone.
.
├── .github/workflows/
├── Dockerfile
├── MAIN.md
├── Makefile
├── README.md
├── README.en.md
├── config/
├── docker-compose.yml
├── output/
├── patches/
├── scripts/
└── tests/
Important entry points:
scripts/build-all.sh: full build for one ABIscripts/build-matrix.sh: serial multi-ABI buildscripts/package-runtime.sh: runtime packagingscripts/redroid-push.sh: copy runtime to Redroid and install wrappersscripts/redroid-test.sh: deploy and validate the runtime inside Androidscripts/redroid-enable-pip.sh: bootstrappipinside the deployed runtime
Host requirements:
- Docker
- Docker Compose v2
- Internet access for source downloads
Android target requirements:
- A writable path such as
/data/local/tmp - Shell access via
docker exec - Working network access if you want to run TLS or HTTPS smoke tests
docker compose build builderdocker compose up -d builderdocker compose exec builder bash -lc "./scripts/build-all.sh"Equivalent Make targets:
make compose-build
make compose-up
make buildThe full build also compiles the native Android wheels for numpy, pandas, Pillow, lxml, and uiautomator2 before installing them into the runtime.
Example for arm64-v8a:
docker compose exec builder bash -lc "ANDROID_ABI=arm64-v8a ./scripts/build-all.sh"Equivalent shortcut:
ANDROID_ABI=arm64-v8a make buildBuild all configured ABIs serially:
docker compose exec builder bash -lc "./scripts/build-matrix.sh"After a successful build, artifacts are written to output/dist/, for example:
python-android-x86_64-full.tar.gzpython-android-x86_64-minimal.tar.gzpython-android-x86_64-slim.tar.gzpython-android-arm64-v8a-full.tar.gzpython-android-arm64-v8a-minimal.tar.gzpython-android-arm64-v8a-slim.tar.gzSHA256SUMSARTIFACTS.txt
The unpacked runtime trees are also available locally under:
output/runtime/full/output/runtime/minimal/output/runtime/slim/
Note: this repository uses output/ as the canonical build output directory. There is no out/ directory in this project.
Native numpy, pandas, Pillow, lxml, and uiautomator2 wheels are written to:
output/wheelhouse/x86_64/output/wheelhouse/arm64-v8a/
full- Expanded stdlib
- Development headers
- pkg-config files
- helper tools
minimal- Main distribution target
- Stdlib mostly zipped
- Keeps
lib-dynload,site-packages,ensurepip, andvenv
slim- Smaller runtime
- Removes headers, manpages, pkg-config files, and extra helper binaries
The build is prepared for common automation and data-processing workflows.
Included stdlib modules relevant to your use case:
jsonsqlite3csvrerandomstringsecretssubprocessossyspathlibtimedatetimebase64hashlibhttp.serverurllib.requestcollectionsitertools
Bundled extra packages:
fakerrequestsjinja2numpypandasPillowlxmluiautomator2
Their pure-Python dependencies are bundled as well:
urllib3certifiidnacharset-normalizerMarkupSafepython-dateutilpytztzdata
numpy, pandas, Pillow, and lxml contain native extensions. uiautomator2 is pure Python, but it depends on native Android wheels such as Pillow and lxml, so it is installed through the native package flow as well.
The default flow now builds these wheels inside the builder with crossenv, Meson, and the Android NDK:
make native-wheels
make native-packages
make package
make exportThe full build already runs this sequence through:
make buildGenerated wheels are written to:
output/wheelhouse/<abi>/
Example for x86_64:
output/wheelhouse/x86_64/numpy-...-cp312-cp312-android_24_x86_64.whl
output/wheelhouse/x86_64/pandas-...-cp312-cp312-android_24_x86_64.whl
Example for arm64-v8a:
output/wheelhouse/arm64-v8a/numpy-...-cp312-cp312-android_24_arm64_v8a.whl
output/wheelhouse/arm64-v8a/pandas-...-cp312-cp312-android_24_arm64_v8a.whl
You can override:
ANDROID_WHEELHOUSE_DIR: directory containing native wheels.ANDROID_WHEEL_PLATFORM_TAG: platform tag passed topip, for exampleandroid_24_x86_64.NUMPY_VERSION: pinnednumpyversion.PANDAS_VERSION: pinnedpandasversion.PILLOW_VERSION: pinnedPillowversion.LXML_VERSION: pinnedlxmlversion.UIAUTOMATOR2_VERSION: pinneduiautomator2version.
If you want to provide your own wheels instead of building them, place them in output/wheelhouse/<abi>/ before running make native-packages.
Each runtime also includes BUNDLED-PACKAGES.txt with the pinned versions that were installed into the final package.
By default, deployment uses the minimal runtime and installs it under:
/data/local/tmp/<abi>/python-android-<abi>
Example for a running Redroid container:
REDROID_CONTAINER=android-15 ./scripts/redroid-push.shExample for arm64-v8a:
REDROID_CONTAINER=android-15 ANDROID_ABI=arm64-v8a ./scripts/redroid-push.shThe deploy step also installs global wrappers:
/system/bin/python3/system/bin/python/system/bin/pip
The deploy script extracts the ABI-specific tarball from:
output/dist/python-android-<abi>-<variant>.tar.gz
It does not copy the generic output/runtime/<variant>/ tree, so switching between x86_64 and arm64-v8a does not deploy a stale runtime.
After deployment:
docker exec android-15 sh -lc 'python3 --version'
docker exec android-15 sh -lc 'python -c "import sys; print(sys.version)"'
docker exec android-15 sh -lc 'python3 -c "import numpy, pandas, uiautomator2; from PIL import Image; from lxml import etree; print(numpy.__version__, pandas.__version__, Image.__version__)"'Enable and test pip in Redroid:
REDROID_CONTAINER=android-15 ./scripts/redroid-enable-pip.sh
docker exec android-15 sh -lc 'pip --version'
docker exec android-15 sh -lc 'pip install urllib3'For arm64-v8a, the Android or Redroid environment must be able to execute ARM64 native ELF binaries. Some x86_64 Redroid images advertise arm64-v8a in Android properties but cannot execute standalone ARM64 binaries.
If you already have a generated runtime and only want to install it into a running Redroid container, you do not need to rebuild anything.
Example using the minimal runtime for x86_64:
REDROID_CONTAINER=android-15 ANDROID_ABI=x86_64 ./scripts/redroid-push.shExample using the minimal runtime for arm64-v8a:
REDROID_CONTAINER=android-15 ANDROID_ABI=arm64-v8a ./scripts/redroid-push.shManual equivalent:
docker cp ./output/dist/python-android-x86_64-minimal.tar.gz android-15:/data/local/tmp/python-android-x86_64-minimal.tar.gz
docker exec android-15 sh -lc 'rm -rf /data/local/tmp/x86_64/python-android-x86_64 && mkdir -p /data/local/tmp/x86_64/python-android-x86_64'
docker exec android-15 sh -lc 'tar -xzf /data/local/tmp/python-android-x86_64-minimal.tar.gz -C /data/local/tmp/x86_64/python-android-x86_64'
docker exec android-15 sh -lc "cat > /system/bin/python3 <<'EOF'
#!/system/bin/sh
exec /data/local/tmp/x86_64/python-android-x86_64/bin/python3 \"\$@\"
EOF
chmod 0755 /system/bin/python3"
docker exec android-15 sh -lc "cat > /system/bin/python <<'EOF'
#!/system/bin/sh
exec /data/local/tmp/x86_64/python-android-x86_64/bin/python3 \"\$@\"
EOF
chmod 0755 /system/bin/python"
docker exec android-15 sh -lc "cat > /system/bin/pip <<'EOF'
#!/system/bin/sh
exec /data/local/tmp/x86_64/python-android-x86_64/bin/python3 -m pip \"\$@\"
EOF
chmod 0755 /system/bin/pip"After deployment:
docker exec android-15 sh -lc 'python3 --version'
docker exec android-15 sh -lc 'python -c "import sys; print(sys.version)"'
docker exec android-15 sh -lc 'python3 -c "import numpy, pandas, uiautomator2; from PIL import Image; from lxml import etree; print(numpy.__version__, pandas.__version__, Image.__version__)"'
docker exec android-15 sh -lc 'pip --version'For a physical device, emulator, or Android VM reachable through adb, use one of the tarballs from output/dist/.
Example with the x86_64 minimal runtime under /data/local/tmp:
adb push output/dist/python-android-x86_64-minimal.tar.gz /data/local/tmp/python-android-x86_64-minimal.tar.gz
adb shell 'rm -rf /data/local/tmp/x86_64/python-android-x86_64 && mkdir -p /data/local/tmp/x86_64/python-android-x86_64'
adb shell 'tar -xzf /data/local/tmp/python-android-x86_64-minimal.tar.gz -C /data/local/tmp/x86_64/python-android-x86_64'
adb shell 'cat > /data/local/tmp/python3 <<'\''EOF'\''
#!/system/bin/sh
export PY_RUNTIME=/data/local/tmp/x86_64/python-android-x86_64
exec "$PY_RUNTIME/bin/python3" "$@"
EOF
chmod 0755 /data/local/tmp/python3'
adb shell '/data/local/tmp/python3 --version'
adb shell '/data/local/tmp/python3 -c "import json, sqlite3, ssl; print(\"ok\")"'
adb shell '/data/local/tmp/python3 -c "import numpy, pandas, uiautomator2; from PIL import Image; from lxml import etree; print(numpy.__version__, pandas.__version__, Image.__version__)"'If you have root or a writable system partition, you can also install the wrappers into /system/bin:
adb root
adb remount
adb shell 'cat > /system/bin/python3 <<'\''EOF'\''
#!/system/bin/sh
exec /data/local/tmp/x86_64/python-android-x86_64/bin/python3 "$@"
EOF
chmod 0755 /system/bin/python3
cat > /system/bin/python <<'\''EOF'\''
#!/system/bin/sh
exec /data/local/tmp/x86_64/python-android-x86_64/bin/python3 "$@"
EOF
chmod 0755 /system/bin/python
cat > /system/bin/pip <<'\''EOF'\''
#!/system/bin/sh
exec /data/local/tmp/x86_64/python-android-x86_64/bin/python3 -m pip "$@"
EOF
chmod 0755 /system/bin/pip'
adb shell 'python3 --version'For pip after an ADB deploy:
adb shell '/data/local/tmp/x86_64/python-android-x86_64/bin/python3 -m ensurepip --default-pip'
adb shell '/data/local/tmp/x86_64/python-android-x86_64/bin/python3 -m pip --version'
adb shell '/data/local/tmp/x86_64/python-android-x86_64/bin/python3 -m pip install urllib3'End users should download prebuilt runtimes from the GitHub Releases page.
Current CI behavior:
- pushes to
mainrun the build workflow and upload temporary workflow artifacts - pushes of tags matching
android-python-py*create a GitHub Release and attach the prebuilt assets
To publish a release with downloadable prebuilt binaries:
git tag android-python-py3.12.12-r1
git push origin android-python-py3.12.12-r1That release will publish assets for:
python-android-x86_64-full.tar.gzpython-android-x86_64-minimal.tar.gzpython-android-x86_64-slim.tar.gzpython-android-arm64-v8a-full.tar.gzpython-android-arm64-v8a-minimal.tar.gzpython-android-arm64-v8a-slim.tar.gzSHA256SUMSARTIFACTS.txt
Run the automated smoke test:
REDROID_CONTAINER=android-15 ./scripts/redroid-test.shOr for arm64-v8a:
REDROID_CONTAINER=android-15 ANDROID_ABI=arm64-v8a ./scripts/redroid-test.shThe test suite covers:
python3 --versionprint("hello from android")- core imports such as
json,ssl,sqlite3, andctypes - file creation under a temporary directory
sys.path- execution of an external
.pyscript - sockets and real TLS/HTTPS checks
faker,requests,jinja2,numpy,pandas,Pillow,lxml, anduiautomator2/system/bin/python3and/system/bin/python
Bootstrap pip in the deployed runtime:
REDROID_CONTAINER=android-15 ./scripts/redroid-enable-pip.shInstall a pure-Python package on the target:
REDROID_CONTAINER=android-15 ./scripts/redroid-pip-install.sh urllib3x86_64 was built, deployed, and tested successfully in Redroid.
arm64-v8a was built successfully and packaged correctly. In the tested Redroid container, the shell still refused to execute the ARM ELF directly even though ro.product.cpu.abilist included arm64-v8a. The failing condition was:
- host shell architecture:
x86_64 - target ELF architecture:
aarch64 - Android linker result: machine mismatch / shell reports
not executable: 64-bit ELF file
This means the image advertises arm64-v8a, but the shell execution path is not exposing native-bridge ARM binary execution to standalone ELF binaries.
If you want Python to ship inside the image instead of being pushed later, the cleanest approach is to treat the runtime as a prebuilt directory tree.
This repository includes a ready-to-adapt example under:
patches/aosp/README.mdpatches/aosp/product-python3.mkpatches/aosp/system-bin/python3patches/aosp/system-bin/pythonpatches/aosp/system-bin/pip
Recommended destination inside the image:
- runtime files:
/system/opt/python3 - command wrappers:
/system/bin/python3/system/bin/python/system/bin/pip
Recommended AOSP flow:
- Build the runtime in Docker.
- Copy
output/runtime/minimal/into a vendor or device repo inside the AOSP tree. - Copy the files from
patches/aosp/. - Include
product-python3.mkfrom your product makefile. - Rebuild the image.
The example uses find-copy-subdir-files so the whole runtime tree can be copied into the image without enumerating every file manually.
This is usually better than inventing an init-time extractor because:
- the files are visible in the final image layout
- updates are explicit
- no first-boot unpack step is needed
- debugging is simpler
- This project does not try to force a fragile single-file static Python binary.
- The practical distribution format is a launcher plus shared runtime and stdlib payload.
readline,curses,tkinter, and other desktop-oriented modules are intentionally out of scope.pipon Android is suitable mainly for pure-Python packages unless you also cross-build the native extensions they require.- Secondary ABI support in Redroid does not automatically mean shell-executable binaries for that ABI.
- Build the desired ABI artifacts in Docker.
- Validate them with
scripts/redroid-test.shon the matching runtime environment. - Publish the generated
.tar.gzartifacts fromoutput/dist/. - Attach
SHA256SUMSandARTIFACTS.txtto the release. - Tag releases using a format such as:
v1.0.0v1.0.0-x86_64v1.0.0-arm64-v8a
make compose-build
make compose-up
make build
REDROID_CONTAINER=android-15 make redroid-test
REDROID_CONTAINER=android-15 make redroid-pip