├── .github └── workflows │ └── test.yml ├── .gitignore ├── Android ├── build_deps.py ├── configure.py ├── unversioned-libpython.patch └── util.py ├── LICENSE ├── README.md ├── build.sh ├── clean.sh ├── devscripts ├── env.sh └── import_all.py ├── docker-build.sh └── src └── .keep /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: push 4 | 5 | # Release-related code borrowed from 6 | # https://github.com/actions/create-release/issues/14#issuecomment-555379810 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | if: contains(github.ref, '-android') 11 | steps: 12 | - name: Create Release 13 | id: create_release 14 | uses: actions/create-release@v1.0.0 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | with: 18 | tag_name: ${{ github.ref }} 19 | release_name: Release ${{ github.ref }} 20 | draft: true 21 | prerelease: true 22 | - name: Output Release URL File 23 | run: echo "${{ steps.create_release.outputs.upload_url }}" > release_url.txt 24 | - name: Save Release URL File for publish 25 | uses: actions/upload-artifact@v1 26 | with: 27 | name: release_url 28 | path: release_url.txt 29 | 30 | build: 31 | runs-on: ubuntu-latest 32 | strategy: 33 | matrix: 34 | env: 35 | - { ARCH: arm, API: 21 } 36 | - { ARCH: arm64, API: 21 } 37 | - { ARCH: x86, API: 21 } 38 | - { ARCH: x86_64, API: 21 } 39 | env: 40 | PYVER: 3.9.0 41 | steps: 42 | - name: Checkout main repo 43 | uses: actions/checkout@v2 44 | - name: Building 45 | run: | 46 | docker run --rm -v $(pwd):/python3-android \ 47 | --env ARCH=${{ matrix.env.ARCH }} \ 48 | --env ANDROID_API=${{ matrix.env.API }} \ 49 | python:$PYVER-slim /python3-android/docker-build.sh 50 | - name: Create package 51 | id: create_package 52 | run: | 53 | sudo apt-get -y update && sudo apt-get -y install libarchive-tools xz-utils 54 | package_filename=python3-android-$PYVER-${{ matrix.env.ARCH }}-${{ matrix.env.API }}.tar.xz 55 | # sudo needed as files created by docker may not be readable by the current user 56 | cd build && sudo bsdtar --xz -cf $package_filename * 57 | echo ::set-output name=package_filename::$package_filename 58 | - name: Load Release URL File from release job 59 | uses: actions/download-artifact@v1 60 | if: contains(github.ref, '-android') 61 | with: 62 | name: release_url 63 | - name: Get Release File Name & Upload URL 64 | id: get_release_info 65 | if: contains(github.ref, '-android') 66 | run: | 67 | value=`cat release_url/release_url.txt` 68 | echo ::set-output name=upload_url::$value 69 | - name: Upload Release Asset 70 | if: contains(github.ref, '-android') 71 | uses: actions/upload-release-asset@v1.0.1 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | with: 75 | upload_url: ${{ steps.get_release_info.outputs.upload_url }} 76 | asset_path: build/${{ steps.create_package.outputs.package_filename }} 77 | asset_name: ${{ steps.create_package.outputs.package_filename }} 78 | asset_content_type: application/x-xz 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /src/* 3 | !/src/.keep 4 | *.pyc 5 | .mypy_cache/ 6 | .gnupg/ 7 | -------------------------------------------------------------------------------- /Android/build_deps.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import shlex 3 | import logging 4 | import os 5 | import re 6 | import subprocess 7 | from typing import List 8 | 9 | from util import ARCHITECTURES, BASE, SYSROOT, env_vars, ndk_unified_toolchain, parse_args 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | class Package: 14 | def __init__(self, target_arch_name: str, android_api_level: int): 15 | self.target_arch_name = target_arch_name 16 | self.target_arch = ARCHITECTURES[target_arch_name] 17 | self.android_api_level = android_api_level 18 | 19 | def run(self, cmd: List[str]): 20 | cwd = BASE / 'deps' / re.sub(r'\.tar\..*', '', os.path.basename(self.source)) 21 | logger.debug(f'Running in {cwd}: ' + ' '.join([shlex.quote(str(arg)) for arg in cmd])) 22 | subprocess.check_call(cmd, cwd=cwd) 23 | 24 | def build(self): 25 | self.configure() 26 | self.make() 27 | self.make_install() 28 | 29 | def configure(self): 30 | self.run([ 31 | './configure', 32 | '--prefix=/usr', 33 | '--libdir=/usr/lib', 34 | '--host=' + self.target_arch.ANDROID_TARGET, 35 | '--disable-shared', 36 | ] + getattr(self, 'configure_args', [])) 37 | 38 | def make(self): 39 | self.run(['make']) 40 | 41 | def make_install(self): 42 | self.run(['make', 'install', f'DESTDIR={SYSROOT}']) 43 | 44 | class BZip2(Package): 45 | source = 'https://sourceware.org/pub/bzip2/bzip2-1.0.8.tar.gz' 46 | 47 | def configure(self): 48 | pass 49 | 50 | def make(self): 51 | self.run([ 52 | 'make', 'libbz2.a', 53 | f'CC={os.environ["CC"]}', 54 | f'CFLAGS={os.environ["CFLAGS"]} {os.environ["CPPFLAGS"]}', 55 | f'AR={os.environ["AR"]}', 56 | f'RANLIB={os.environ["RANLIB"]}', 57 | ]) 58 | 59 | def make_install(self): 60 | # The install target in bzip2's Makefile needs too many fixes - 61 | # Installing files manually is simpler. 62 | self.run(['install', '-Dm644', 'libbz2.a', '-t', str(SYSROOT / 'usr' / 'lib')]) 63 | self.run(['install', '-Dm644', 'bzlib.h', '-t', str(SYSROOT / 'usr' / 'include')]) 64 | 65 | class GDBM(Package): 66 | source = 'https://ftp.gnu.org/gnu/gdbm/gdbm-1.18.1.tar.gz' 67 | configure_args = ['--enable-libgdbm-compat'] 68 | 69 | class LibFFI(Package): 70 | source = 'https://github.com/libffi/libffi/releases/download/v3.3/libffi-3.3.tar.gz' 71 | # libffi may fail to configure with Docker on WSL2 (#33) 72 | configure_args = ['--disable-builddir'] 73 | 74 | class LibUUID(Package): 75 | source = 'https://mirrors.edge.kernel.org/pub/linux/utils/util-linux/v2.36/util-linux-2.36.tar.xz' 76 | configure_args = ['--disable-all-programs', '--enable-libuuid'] 77 | 78 | class NCurses(Package): 79 | source = 'https://invisible-mirror.net/archives/ncurses/ncurses-6.2.tar.gz' 80 | # Not stripping the binaries as there is no easy way to specify the strip program for Android 81 | configure_args = ['--without-ada', '--enable-widec', '--without-debug', '--without-cxx-binding', '--disable-stripping'] 82 | 83 | class OpenSSL(Package): 84 | source = 'https://www.openssl.org/source/openssl-1.1.1h.tar.gz' 85 | 86 | def configure(self): 87 | # OpenSSL handles NDK internal paths by itself 88 | path = os.pathsep.join(( 89 | # OpenSSL requires NDK's clang in $PATH to enable usage of clang 90 | str(ndk_unified_toolchain()), 91 | # and it requires unprefixed binutils, too 92 | str(ndk_unified_toolchain().parent / self.target_arch.ANDROID_TARGET / 'bin'), 93 | os.environ['PATH'], 94 | )) 95 | 96 | logger.debug(f'$PATH for OpenSSL: {path}') 97 | 98 | os.environ['PATH'] = path 99 | 100 | openssl_target = 'android-' + self.target_arch_name 101 | 102 | self.run(['./Configure', '--prefix=/usr', '--openssldir=/etc/ssl', openssl_target, 103 | 'no-shared', 'no-tests', f'-D__ANDROID_API__={self.android_api_level}']) 104 | 105 | def make_install(self): 106 | self.run(['make', 'install_sw', 'install_ssldirs', f'DESTDIR={SYSROOT}']) 107 | 108 | class Readline(Package): 109 | source = 'https://ftp.gnu.org/gnu/readline/readline-8.0.tar.gz' 110 | 111 | # See the wcwidth() test in aclocal.m4. Tested on Android 6.0 and it's broken 112 | # XXX: wcwidth() is implemented in [1], which may be in Android P 113 | # Need a conditional configuration then? 114 | # [1] https://android.googlesource.com/platform/bionic/+/c41b560f5f624cbf40febd0a3ec0b2a3f74b8e42 115 | configure_args = ['bash_cv_wcwidth_broken=yes'] 116 | 117 | class SQLite(Package): 118 | source = 'https://sqlite.org/2020/sqlite-autoconf-3330000.tar.gz' 119 | 120 | class XZ(Package): 121 | source = 'https://tukaani.org/xz/xz-5.2.5.tar.xz' 122 | 123 | class ZLib(Package): 124 | source = 'https://www.zlib.net/zlib-1.2.11.tar.gz' 125 | 126 | def configure(self): 127 | os.environ.update({ 128 | 'CHOST': self.target_arch.ANDROID_TARGET + '-', 129 | 'CFLAGS': ' '.join([os.environ['CPPFLAGS'], os.environ['CFLAGS']]), 130 | }) 131 | 132 | self.run([ 133 | './configure', 134 | '--prefix=/usr', 135 | '--static', 136 | ]) 137 | 138 | def make(self): 139 | self.run(['make', 'libz.a']) 140 | 141 | def build_package(pkg: Package): 142 | subprocess.check_call(['curl', '-fLO', pkg.source], cwd=BASE / 'deps') 143 | subprocess.check_call(['tar', '--no-same-owner', '-xf', os.path.basename(pkg.source)], cwd=BASE / 'deps') 144 | 145 | try: 146 | saved_env = os.environ.copy() 147 | pkg.build() 148 | finally: 149 | os.environ.clear() 150 | os.environ.update(saved_env) 151 | 152 | def main(): 153 | logging.basicConfig(level=logging.DEBUG) 154 | 155 | args, _ = parse_args() 156 | 157 | os.environ.update(env_vars(args.target_arch_name, args.android_api_level)) 158 | 159 | (BASE / 'deps').mkdir(exist_ok=True) 160 | SYSROOT.mkdir(exist_ok=True) 161 | 162 | package_classes = ( 163 | # ncurses is a dependency of readline 164 | NCurses, 165 | BZip2, GDBM, LibFFI, LibUUID, OpenSSL, Readline, SQLite, XZ, ZLib, 166 | ) 167 | 168 | for pkg_cls in package_classes: 169 | build_package(pkg_cls(args.target_arch_name, args.android_api_level)) 170 | 171 | if __name__ == '__main__': 172 | main() 173 | -------------------------------------------------------------------------------- /Android/configure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | 4 | from util import ARCHITECTURES, env_vars, parse_args 5 | 6 | def main(): 7 | args, remaining = parse_args() 8 | os.environ.update(env_vars(args.target_arch_name, args.android_api_level)) 9 | 10 | # CPython requires explicit --build, and its value does not matter 11 | # (e.g., x86_64-linux-gnu should also work on macOS) 12 | cmd = [ 13 | 'bash', './configure', 14 | '--host=' + ARCHITECTURES[args.target_arch_name].ANDROID_TARGET, 15 | '--build=x86_64-linux-gnu', 16 | 'ac_cv_file__dev_ptmx=yes', 17 | 'ac_cv_file__dev_ptc=no', 18 | 'ac_cv_buggy_getaddrinfo=no', # for IPv6 functionality 19 | ] 20 | 21 | os.execvp('bash', cmd + remaining) 22 | 23 | if __name__ == '__main__': 24 | main() 25 | -------------------------------------------------------------------------------- /Android/unversioned-libpython.patch: -------------------------------------------------------------------------------- 1 | diff --git a/configure.ac b/configure.ac 2 | index 73ee71c6d2..a62edaebae 100644 3 | --- a/configure.ac 4 | +++ b/configure.ac 5 | @@ -1121,7 +1121,10 @@ if test $enable_shared = "yes"; then 6 | LDLIBRARY='libpython$(LDVERSION).so' 7 | BLDLIBRARY='-L. -lpython$(LDVERSION)' 8 | RUNSHARED=LD_LIBRARY_PATH=`pwd`${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}} 9 | - INSTSONAME="$LDLIBRARY".$SOVERSION 10 | + if test $ac_sys_system != Linux-android 11 | + then 12 | + INSTSONAME="$LDLIBRARY".$SOVERSION 13 | + fi 14 | if test "$with_pydebug" != yes 15 | then 16 | PY3LIBRARY=libpython3.so 17 | -------------------------------------------------------------------------------- /Android/util.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import pathlib 4 | from dataclasses import dataclass 5 | from typing import Dict, Optional 6 | 7 | BASE = pathlib.Path(__file__).resolve().parent 8 | SYSROOT = BASE / 'sysroot' 9 | 10 | 11 | @dataclass 12 | class Arch: 13 | ANDROID_TARGET: str 14 | BINUTILS_PREFIX: Optional[str] = None 15 | 16 | @property 17 | def binutils_prefix(self) -> str: 18 | return self.BINUTILS_PREFIX or self.ANDROID_TARGET 19 | 20 | 21 | ARCHITECTURES = { 22 | 'arm': Arch('armv7a-linux-androideabi', 'arm-linux-androideabi'), 23 | 'arm64': Arch('aarch64-linux-android',), 24 | 'x86': Arch('i686-linux-android',), 25 | 'x86_64': Arch('x86_64-linux-android',), 26 | } 27 | 28 | def ndk_unified_toolchain() -> pathlib.Path: 29 | ndk_path = os.getenv('ANDROID_NDK') 30 | if not ndk_path: 31 | raise Exception('Requires environment variable $ANDROID_NDK') 32 | ndk = pathlib.Path(ndk_path) 33 | 34 | HOST_OS = os.uname().sysname.lower() 35 | 36 | path = ndk / 'toolchains' / 'llvm' / 'prebuilt' / f'{HOST_OS}-x86_64' / 'bin' 37 | 38 | if not path.exists(): 39 | raise Exception('Requires Android NDK r19 or above') 40 | 41 | return path 42 | 43 | 44 | def env_vars(target_arch_name: str, android_api_level: int) -> Dict[str, str]: 45 | target_arch = ARCHITECTURES[target_arch_name] 46 | 47 | CLANG_PREFIX = (ndk_unified_toolchain() / 48 | f'{target_arch.ANDROID_TARGET}{android_api_level}') 49 | 50 | env = { 51 | # Compilers 52 | 'CC': f'{CLANG_PREFIX}-clang', 53 | 'CXX': f'{CLANG_PREFIX}-clang++', 54 | 'CPP': f'{CLANG_PREFIX}-clang -E', 55 | 56 | # Compiler flags 57 | 'CPPFLAGS': f'-I{SYSROOT}/usr/include', 58 | 'CFLAGS': '-fPIC', 59 | 'CXXLAGS': '-fPIC', 60 | 'LDFLAGS': f'-L{SYSROOT}/usr/lib -pie', 61 | 62 | # pkg-config settings 63 | 'PKG_CONFIG_SYSROOT_DIR': str(SYSROOT), 64 | 'PKG_CONFIG_LIBDIR': str(SYSROOT / 'usr' / 'lib' / 'pkgconfig'), 65 | 66 | 'PYTHONPATH': str(BASE), 67 | } 68 | 69 | for prog in ('ar', 'as', 'nm', 'objcopy', 'objdump', 'ranlib', 'readelf', 'strip'): 70 | env[prog.upper()] = str(ndk_unified_toolchain() / f'llvm-{prog}') 71 | env['ld'] = str(ndk_unified_toolchain() / 'lld') 72 | 73 | return env 74 | 75 | 76 | def parse_args(): 77 | parser = argparse.ArgumentParser() 78 | parser.add_argument('--arch', required=True, choices=ARCHITECTURES.keys(), dest='target_arch_name') 79 | parser.add_argument('--api', required=True, type=int, choices=range(21, 30), dest='android_api_level') 80 | return parser.parse_known_args() 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # BSD Zero Clause License (SPDX: 0BSD) 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 10 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 11 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 12 | THIS SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Python 3 Android 2 | ================ 3 | 4 | This is an experimental set of build scripts that will cross-compile Python 3.9.0 for an Android device. 5 | 6 | Prerequisites 7 | ------------- 8 | 9 | Building requires: 10 | 11 | 1. Linux. This project might work on other systems supported by NDK but no guarantee. 12 | 2. Android NDK r21 installed and environment variable ``$ANDROID_NDK`` points to its root directory. Older NDK may not work and NDK <= r18 is known to be incompatible. 13 | 3. `python3.9` binary from Python 3.9.0 on the building host. It's recommended to use exactly that Python version, which can be installed via [pyenv](https://github.com/yyuu/pyenv). Don't forget to check that `python3.9` is available in $PATH. 14 | 4. `tic` binary from ncurses 6.2 on the building host. Slightly newer or older version may also work but no guarantee. 15 | 5. A case-sensitive filesystem. The default filesystem on Windows and macOS is case-insensitive, and building may fail. 16 | 17 | Running requires: 18 | 19 | 1. Android 5.0 (Lollipop, API 21) or above 20 | 2. arm, arm64, x86 or x86-64 21 | 22 | Build 23 | ----- 24 | 25 | 1. Run `./clean.sh` for good measure. 26 | 2. For every API Level/architecture combination you wish to build for: 27 | * `ARCH=arm ANDROID_API=21 ./build.sh` to build everything! 28 | 29 | Build using Docker/Podman 30 | ------------------ 31 | 32 | Download the latest NDK for Linux from https://developer.android.com/ndk/downloads and extract it. 33 | 34 | ``` 35 | docker run --rm -it -v $(pwd):/python3-android -v /path/to/android-ndk:/android-ndk:ro --env ARCH=arm --env ANDROID_API=21 python:3.9.0-slim /python3-android/docker-build.sh 36 | ``` 37 | 38 | Here `/path/to/android-ndk` should be replaced with the actual for NDK (e.g., `/opt/android-ndk`). 39 | 40 | Podman is also supported. Simply replace `docker` with `podman` in the command above. 41 | 42 | Installation 43 | ------------ 44 | 45 | 1. Make sure `adb shell` works fine 46 | 2. Copy all files in `build` to a folder on the device (e.g., ```/data/local/tmp/python3```). Note that on most devices `/sdcard` is not on a POSIX-compliant filesystem, so the python binary will not run from there. 47 | 3. In adb shell: 48 |
49 | cd /data/local/tmp/build
50 | . ./env.sh
51 | python3
52 | 
53 | And have fun! 54 | 55 | SSL/TLS 56 | ------- 57 | SSL certificates have old and new naming schemes. Android uses the old scheme yet the latest OpenSSL uses the new one. If you got ```CERTIFICATE_VERIFY_FAILED``` when using SSL/TLS in Python, you need to collect system certificates: (thanks @GRRedWings for the idea) 58 | ``` 59 | cd /data/local/tmp/build 60 | mkdir -p etc/ssl 61 | cat /system/etc/security/cacerts/* > etc/ssl/cert.pem 62 | ``` 63 | Path for certificates may vary with device vendor and/or Android version. Note that this approach only collects system certificates. If you need to collect user-installed certificates, most likely root access on your Android device is needed. 64 | 65 | Check SSL/TLS functionality with: 66 | ``` 67 | import urllib.request 68 | print(urllib.request.urlopen('https://httpbin.org/ip').read().decode('ascii')) 69 | ``` 70 | 71 | 72 | Known Issues 73 | ------------ 74 | 75 | No big issues! yay 76 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | THIS_DIR="$PWD" 7 | 8 | PYVER=3.9.0 9 | SRCDIR=src/Python-$PYVER 10 | 11 | COMMON_ARGS="--arch ${ARCH:-arm} --api ${ANDROID_API:-21}" 12 | 13 | if [ ! -d $SRCDIR ]; then 14 | mkdir -p src 15 | pushd src 16 | curl -vLO https://www.python.org/ftp/python/$PYVER/Python-$PYVER.tar.xz 17 | # Use --no-same-owner so that files extracted are still owned by the 18 | # running user in a rootless container 19 | tar --no-same-owner -xf Python-$PYVER.tar.xz 20 | popd 21 | fi 22 | 23 | cp -r Android $SRCDIR 24 | pushd $SRCDIR 25 | patch -Np1 -i ./Android/unversioned-libpython.patch 26 | autoreconf -ifv 27 | ./Android/build_deps.py $COMMON_ARGS 28 | ./Android/configure.py $COMMON_ARGS --prefix=/usr "$@" 29 | make 30 | make install DESTDIR="$THIS_DIR/build" 31 | popd 32 | cp -r $SRCDIR/Android/sysroot/usr/share/terminfo build/usr/share/ 33 | cp devscripts/env.sh build/ 34 | -------------------------------------------------------------------------------- /clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm -rf src/* build 3 | -------------------------------------------------------------------------------- /devscripts/env.sh: -------------------------------------------------------------------------------- 1 | export PATH=$PATH:$PWD/usr/bin 2 | if [ ! -z "$LD_LIBRARY_PATH" ] ; then 3 | export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:" 4 | fi 5 | export LD_LIBRARY_PATH="$LD_LIBRARY_PATH$PWD/usr/lib" 6 | export SSL_CERT_FILE="$PWD/etc/ssl/cert.pem" 7 | # For ncurses 8 | export TERMINFO="$PWD/usr/share/terminfo" 9 | 10 | export HOME=/data/local/tmp 11 | -------------------------------------------------------------------------------- /devscripts/import_all.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os.path 3 | 4 | mod_path = os.path.join( 5 | sys.prefix, 6 | 'lib/python%d.%d/lib-dynload' % (sys.version_info[0], sys.version_info[1])) 7 | 8 | for mod_filename in os.listdir(mod_path): 9 | mod_name = mod_filename.split('.')[0] 10 | try: 11 | mod = __import__(mod_name) 12 | except ImportError as e: 13 | print(mod_name) 14 | print(e) 15 | -------------------------------------------------------------------------------- /docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | apt-get update -y 7 | apt-get install -y autoconf automake cmake gawk gettext git gcc make patch pkg-config 8 | 9 | export ANDROID_NDK=/android-ndk 10 | 11 | if [ ! -d "$ANDROID_NDK" ] ; then 12 | # In general we don't want download NDK for every build, but it is simpler to do it here 13 | # for CI builds 14 | NDK_VER=r21d 15 | apt-get install -y wget bsdtar 16 | wget --no-verbose https://dl.google.com/android/repository/android-ndk-$NDK_VER-linux-x86_64.zip 17 | bsdtar xf android-ndk-${NDK_VER}-linux-x86_64.zip 18 | ANDROID_NDK=/android-ndk-$NDK_VER 19 | fi 20 | 21 | cd /python3-android 22 | 23 | ./build.sh "$@" 24 | -------------------------------------------------------------------------------- /src/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yan12125/python3-android/3dfc0439fa8e2cd58da4de5b1af9906cdea53ff9/src/.keep --------------------------------------------------------------------------------