Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Normalize line endings for files that run in Linux contexts.
# All shell scripts, init files, and Linux config files MUST have LF endings;
# Windows CRLF line endings break shebang parsing and OpenRC service scripts.

# Default: let git auto-detect text vs binary
* text=auto

# Force LF for all shell scripts and Linux-targeted text files
*.sh text eol=lf
*.c text eol=lf
*.h text eol=lf
Dockerfile* text eol=lf
Makefile* text eol=lf
*.mk text eol=lf
*.conf text eol=lf
*.md text eol=lf
*.txt text eol=lf
*.gradle text eol=lf
*.kts text eol=lf
*.kt text eol=lf
*.java text eol=lf
*.xml text eol=lf
*.json text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
*.toml text eol=lf
*.properties text eol=lf
*.config text eol=lf

# Explicitly force LF for files without extensions that are Linux scripts
init-podroid text eol=lf
build-rootfs/files/etc/init.d/podroid-bootstrap text eol=lf
build-rootfs/files/etc/init.d/podroid-network text eol=lf
build-rootfs/files/etc/init.d/podroid-ready text eol=lf
build-rootfs/files/etc/init.d/podroid-resize text eol=lf
build-rootfs/files/etc/init.d/podroid-x11 text eol=lf
build-rootfs/files/etc/init.d/podroid-vsock text eol=lf
build-rootfs/files/etc/init.d/podroid-hostd text eol=lf
build-rootfs/files/etc/init.d/podroid-migrate text eol=lf
build-rootfs/files/etc/inittab text eol=lf
build-rootfs/files/etc/rc.conf text eol=lf
build-rootfs/files/etc/conf.d/podroid text eol=lf
build-rootfs/files/usr/local/bin/podroid-getty text eol=lf
build-rootfs/files/usr/local/bin/podroid-login text eol=lf
build-rootfs/files/usr/local/bin/podroid-resize text eol=lf
build-rootfs/files/etc/podroid/forwards.conf text eol=lf

# Binary assets — never convert
*.squashfs binary
*.img binary
*.so binary
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.webp binary
*.ico binary
*.ttf binary
*.otf binary
*.woff binary
*.woff2 binary
*.apk binary
*.jks binary
*.keystore binary
*.zip binary
*.gz binary
*.bz2 binary
*.xz binary
*.zst binary
9 changes: 7 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Guidance for Claude Code (and any AI assistant or new contributor) working in th

## What Is This

Podroid is an Android app that runs a real **Alpine 3.23** Linux VM on stock Android 9+ (arm64) to provide rootless **Podman / Docker / LXC** containers and an in-app **X11 desktop** - no root, no custom recovery.
Podroid is an Android app that runs a real **Alpine 3.23** Linux VM on stock Android 8+ (arm64) to provide rootless **Podman / Docker / LXC** containers and an in-app **X11 desktop** - no root, no custom recovery.

- **Two interchangeable VM backends** behind one interface (`VmEngine`):
- **QEMU (TCG)** - software emulation, the default, needs no special permission.
Expand All @@ -18,7 +18,7 @@ Podroid is an Android app that runs a real **Alpine 3.23** Linux VM on stock And
|---|---|
| Package | `com.excp.podroid` (debug: `com.excp.podroid.debug`) |
| Version | `versionName` / `versionCode` in `app/build.gradle.kts` |
| Min / target SDK | 28 (Android 9) / 36 |
| Min / target SDK | 26 (Android 8) / 36 |
| Architecture | arm64 (`aarch64`) only |
| Guest | Alpine 3.23 squashfs + persistent ext4 overlay, OpenRC PID 1 |
| Kernel | custom Linux, version pinned by `podroidKernelVersion` in `gradle.properties` |
Expand Down Expand Up @@ -166,6 +166,8 @@ Single-activity Compose app: `ui/navigation/NavGraph.kt` routes `setup → home
├── podroid-bridge.c # PTY <-> virtio-console relay -> libpodroid-bridge.so
├── podroid-launcher.c # exec wrapper that ties QEMU's lifetime to the app -> libpodroid-launcher.so
├── Dockerfile # kernel + initramfs + QEMU build
├── build-tools/ # static assets used during Docker builds
│ └── cross-android-aarch64.ini # Meson cross-compilation config for aarch64-android26
├── build-rootfs/
│ ├── Dockerfile.rootfs, build-rootfs.sh
│ ├── vsock-agent/ # podroid-vsock-agent.c (AVF control/forward agent)
Expand Down Expand Up @@ -207,6 +209,9 @@ In `QemuEngine.buildCommand()`: `tcg,thread=multi`, larger `tb-size` for ≥2GB

## Quirks & gotchas

- **`CONFIG_DEVTMPFS_MOUNT=y` pre-mounts `/dev` before `/init` runs.** `init-podroid` must use `mount ... || true` for the devtmpfs line — the kernel's pre-populated `/dev` is sufficient and a second `mount(2)` returns EBUSY. Also: `size=` is not valid for devtmpfs in kernels where it is ramfs-backed (causes EINVAL). Either failure exits util-linux with `MNT_EX_FAIL=32`; `set -e` propagates `exit(32)`, encoded as `exitcode=0x00002000` ("Attempted to kill init!"). Always keep the `|| true`.
- **util-linux `mount` stops option parsing at the first non-option argument** when `POSIXLY_CORRECT` is set. Put `-o` flags *before* device and mountpoint (`mount -t proc -o noexec,nosuid,nodev proc /proc`), not after. Options placed after the mountpoint are silently dropped, causing `mount(2)` to be called with `flags=0` and returning EPERM — which util-linux prints as "must be superuser to use mount" even for root.
- **`Dockerfile` heredoc gotcha.** Docker BuildKit's parser treats `[section]` lines inside `RUN ... << 'EOF'` heredocs as unknown Dockerfile instructions and aborts the parse. Instead of using shell heredocs in `RUN` for multi-line config files, use `COPY build-tools/<file>` from the build context. The Meson cross-compilation config lives in `build-tools/cross-android-aarch64.ini` for this reason.
- **Backend asymmetry is the #1 source of bugs.** QEMU = SLIRP + QMP + virtio-console (TTY); AVF = DHCP + vsock (raw sockets). Test both. The host bridge's `cfmakeraw` on hvc2 (above) is a concrete example.
- **Boot detection** scans a rolling buffer, not per-read chunks (fast devices split markers).
- **Bridge stderr is silenced** (`dup2(/dev/null, STDERR)`): it runs as a `TerminalSession` subprocess whose stderr IS the PTY. Never add `fprintf(stderr, ...)` to `podroid-bridge.c`.
Expand Down
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ You will need:
- **Docker 20.10+** for the kernel, initramfs, rootfs, and QEMU build pipelines
- **Android NDK r27c** for the bridge and Termux native libraries
- **Android SDK** with platform 36 + build-tools
- An **arm64 Android device** running **Android 9.0+ (API 28)** for testing
- An **arm64 Android device** running **Android 8.0+ (API 26)** for testing

## Build pipeline

Expand Down Expand Up @@ -79,6 +79,8 @@ Podroid/
├── init-podroid Minimal initramfs script (~45 lines)
├── podroid-bridge.c Native PTY ↔ virtio-console relay
├── Dockerfile Kernel + initramfs + QEMU build pipeline
├── build-tools/ Static assets used during Docker builds
│ └── cross-android-aarch64.ini Meson cross-compilation config for aarch64-android26
├── build-rootfs/ Alpine squashfs build pipeline
│ ├── Dockerfile.rootfs
│ ├── build-rootfs.sh
Expand Down
48 changes: 17 additions & 31 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -219,43 +219,29 @@ RUN pip3 install --break-system-packages meson 2>/dev/null || pip3 install meson
RUN wget -q https://dl.google.com/android/repository/android-ndk-r27c-linux.zip -O /tmp/ndk.zip \
&& unzip -q /tmp/ndk.zip -d /opt && mv /opt/android-ndk-r27c /opt/ndk && rm /tmp/ndk.zip
ENV NDK=/opt/ndk LLVM=/opt/ndk/toolchains/llvm/prebuilt/linux-x86_64 PREFIX=/opt/deps
ENV CC="${LLVM}/bin/aarch64-linux-android28-clang" AR="${LLVM}/bin/llvm-ar" RANLIB="${LLVM}/bin/llvm-ranlib"
ENV CC="${LLVM}/bin/aarch64-linux-android26-clang" AR="${LLVM}/bin/llvm-ar" RANLIB="${LLVM}/bin/llvm-ranlib"
RUN mkdir -p ${PREFIX}/{lib,include,lib/pkgconfig}

# Cross-compilation setup
RUN printf '#!/bin/sh\nexport PKG_CONFIG_LIBDIR=/opt/deps/lib/pkgconfig\nexport PKG_CONFIG_PATH=\nexec pkg-config "$@"\n' \
> /usr/local/bin/aarch64-android-pkg-config && chmod +x /usr/local/bin/aarch64-android-pkg-config \
&& ln -s /usr/local/bin/aarch64-android-pkg-config ${LLVM}/bin/llvm-pkg-config

RUN cat > /opt/cross-android-aarch64.ini << 'EOF'
[binaries]
c = '/opt/ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android28-clang'
cpp = '/opt/ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android28-clang++'
ar = '/opt/ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar'
strip = '/opt/ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip'
ranlib = '/opt/ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ranlib'
nm = '/opt/ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-nm'
pkg-config = '/usr/local/bin/aarch64-android-pkg-config'
[properties]
sys_root = '/opt/ndk/toolchains/llvm/prebuilt/linux-x86_64/sysroot'
pkg_config_libdir = ['/opt/deps/lib/pkgconfig']
c_args = ['--sysroot=/opt/ndk/toolchains/llvm/prebuilt/linux-x86_64/sysroot', '-target', 'aarch64-linux-android28', '-I/opt/deps/include', '-fPIC', '-O2', '-march=armv8-a']
cpp_args = ['--sysroot=/opt/ndk/toolchains/llvm/prebuilt/linux-x86_64/sysroot', '-target', 'aarch64-linux-android28', '-I/opt/deps/include', '-fPIC', '-O2', '-march=armv8-a']
c_link_args = ['--sysroot=/opt/ndk/toolchains/llvm/prebuilt/linux-x86_64/sysroot', '-target', 'aarch64-linux-android28', '-L/opt/deps/lib', '-Wl,-z,max-page-size=16384']
cpp_link_args = ['--sysroot=/opt/ndk/toolchains/llvm/prebuilt/linux-x86_64/sysroot', '-target', 'aarch64-linux-android28', '-L/opt/deps/lib', '-Wl,-z,max-page-size=16384']
[host_machine]
system = 'linux'
cpu_family = 'aarch64'
cpu = 'aarch64'
endian = 'little'
EOF
COPY build-tools/cross-android-aarch64.ini /opt/cross-android-aarch64.ini

# Deps (pcre2, libffi, glib, pixman, libattr, libucontext)
# Deps (pcre2, libffi, libiconv, glib, pixman, libattr, libucontext)
RUN wget -q https://github.com/PCRE2Project/pcre2/releases/download/pcre2-10.44/pcre2-10.44.tar.gz && tar xf pcre2-10.44.tar.gz && cd pcre2-10.44 && ./configure --host=aarch64-linux-android --prefix=${PREFIX} --enable-static --disable-shared CC="${CC}" && make -j$(nproc) install
RUN wget -q https://github.com/libffi/libffi/releases/download/v3.4.6/libffi-3.4.6.tar.gz && tar xf libffi-3.4.6.tar.gz && cd libffi-3.4.6 && ./configure --host=aarch64-linux-android --prefix=${PREFIX} --enable-static --disable-shared CC="${CC}" && make -j$(nproc) install
RUN wget -q https://download.gnome.org/sources/glib/2.82/glib-2.82.5.tar.xz && tar xf glib-2.82.5.tar.xz && cd glib-2.82.5 && meson setup _build --cross-file /opt/cross-android-aarch64.ini --prefix ${PREFIX} --default-library static -Dselinux=disabled -Dlibmount=disabled && ninja -C _build install
# API-26 Bionic lacks iconv symbols in libc. Provide a tiny libiconv shim so
# glib can link in the cross build. It implements byte-for-byte passthrough.
RUN printf '#ifndef PODROID_ICONV_H\n#define PODROID_ICONV_H\n#include <stddef.h>\ntypedef void *iconv_t;\niconv_t iconv_open(const char *tocode, const char *fromcode);\nsize_t iconv(iconv_t cd, char **inbuf, size_t *inbytesleft, char **outbuf, size_t *outbytesleft);\nint iconv_close(iconv_t cd);\n#endif\n' > ${PREFIX}/include/iconv.h \
&& printf '#include <errno.h>\n#include <stddef.h>\n#include <string.h>\n#include <iconv.h>\niconv_t iconv_open(const char *tocode, const char *fromcode) {\n if (!tocode || !fromcode) { errno = EINVAL; return (iconv_t)-1; }\n return (iconv_t)1;\n}\nsize_t iconv(iconv_t cd, char **inbuf, size_t *inbytesleft, char **outbuf, size_t *outbytesleft) {\n (void)cd;\n if (!inbuf || !inbytesleft || !outbuf || !outbytesleft) { errno = EINVAL; return (size_t)-1; }\n if (!*inbuf || *inbytesleft == 0) return 0;\n if (!*outbuf || *outbytesleft == 0) { errno = E2BIG; return (size_t)-1; }\n size_t n = (*inbytesleft < *outbytesleft) ? *inbytesleft : *outbytesleft;\n memcpy(*outbuf, *inbuf, n);\n *inbuf += n;\n *outbuf += n;\n *inbytesleft -= n;\n *outbytesleft -= n;\n if (*inbytesleft != 0) { errno = E2BIG; return (size_t)-1; }\n return 0;\n}\nint iconv_close(iconv_t cd) {\n (void)cd;\n return 0;\n}\n' > /tmp/iconv_shim.c \
&& ${CC} --sysroot=${LLVM}/sysroot -target aarch64-linux-android26 -I${PREFIX}/include -c /tmp/iconv_shim.c -o /tmp/iconv_shim.o \
&& ${AR} rcs ${PREFIX}/lib/libiconv.a /tmp/iconv_shim.o \
&& cp ${PREFIX}/lib/libiconv.a ${LLVM}/sysroot/usr/lib/aarch64-linux-android/26/libiconv.a
RUN wget -q https://download.gnome.org/sources/glib/2.82/glib-2.82.5.tar.xz&& tar xf glib-2.82.5.tar.xz && cd glib-2.82.5 && meson setup _build --cross-file /opt/cross-android-aarch64.ini --prefix ${PREFIX} --default-library static -Dselinux=disabled -Dlibmount=disabled && ninja -C _build install
RUN wget -q https://cairographics.org/releases/pixman-0.44.2.tar.xz && tar xf pixman-0.44.2.tar.xz && cd pixman-0.44.2 && meson setup _build --cross-file /opt/cross-android-aarch64.ini --prefix ${PREFIX} --default-library static -Da64-neon=disabled && ninja -C _build install
RUN wget -q https://download.savannah.gnu.org/releases/attr/attr-2.5.2.tar.gz && tar xf attr-2.5.2.tar.gz && cd attr-2.5.2 && ./configure --host=aarch64-linux-android --prefix=${PREFIX} --enable-static --disable-shared CC="${CC}" && make -j$(nproc) install && cp ${PREFIX}/lib/libattr.a ${LLVM}/sysroot/usr/lib/aarch64-linux-android/28/libattr.a
RUN wget -q https://download.savannah.gnu.org/releases/attr/attr-2.5.2.tar.gz && tar xf attr-2.5.2.tar.gz && cd attr-2.5.2 && ./configure --host=aarch64-linux-android --prefix=${PREFIX} --enable-static --disable-shared CC="${CC}" && make -j$(nproc) install && cp ${PREFIX}/lib/libattr.a ${LLVM}/sysroot/usr/lib/aarch64-linux-android/26/libattr.a
RUN git clone --depth=1 https://github.com/kaniini/libucontext.git /tmp/libucontext && make -C /tmp/libucontext ARCH=aarch64 CC="${CC}" EXPORT_UNPREFIXED=yes && install -Dm644 /tmp/libucontext/libucontext.a ${PREFIX}/lib/libucontext.a && install -Dm644 /tmp/libucontext/include/libucontext/libucontext.h ${PREFIX}/include/libucontext/libucontext.h && install -Dm644 /tmp/libucontext/arch/common/include/libucontext/bits.h ${PREFIX}/include/libucontext/bits.h \
&& printf '#ifndef PODROID_UCONTEXT_SHIM_H\n#define PODROID_UCONTEXT_SHIM_H\n#include_next <ucontext.h>\n#include <libucontext/libucontext.h>\n#define getcontext libucontext_getcontext\n#define makecontext libucontext_makecontext\n#define setcontext libucontext_setcontext\n#define swapcontext libucontext_swapcontext\n#endif\n' > ${PREFIX}/include/ucontext.h

Expand All @@ -275,14 +261,14 @@ RUN printf '#undef st_atime_nsec\n#undef st_mtime_nsec\n#undef st_ctime_nsec\n'
# ivshmem-{server,client} also call shm_open; stub their meson.build files since we don't ship them
RUN printf '# disabled for Android Bionic\n' > ${QEMU_DIR}/contrib/ivshmem-server/meson.build \
&& printf '# disabled for Android Bionic\n' > ${QEMU_DIR}/contrib/ivshmem-client/meson.build
# shm_open/shm_unlink are absent from the NDK API-28 stubs.
# shm_open/shm_unlink are absent from the NDK API-26 stubs.
# Shim header: forward-declares them for all QEMU TUs.
# libshm.a: provides an implementation via memfd_create (works on all Android 8+ kernels).
RUN printf '#ifndef PODROID_SHM_SHIM_H\n#define PODROID_SHM_SHIM_H\nextern int shm_open(const char *, int, unsigned);\nextern int shm_unlink(const char *);\n#endif\n' \
> /opt/shm_shim.h
RUN printf '#include <sys/syscall.h>\n#include <unistd.h>\n#include <errno.h>\n#ifndef SYS_memfd_create\n#define SYS_memfd_create 279\n#endif\nint shm_open(const char *n, int f, unsigned m) {\n (void)f; (void)m;\n while (*n == '"'"'/'"'"') n++;\n long fd = syscall(SYS_memfd_create, n, 0);\n if (fd < 0) { errno = (int)(-fd); return -1; }\n return (int)fd;\n}\nint shm_unlink(const char *n) { (void)n; return 0; }\n' \
> /tmp/shm_stub.c \
&& ${CC} --sysroot=${LLVM}/sysroot -target aarch64-linux-android28 -c /tmp/shm_stub.c -o /tmp/shm_stub.o \
&& ${CC} --sysroot=${LLVM}/sysroot -target aarch64-linux-android26 -c /tmp/shm_stub.c -o /tmp/shm_stub.o \
&& ${AR} rcs ${PREFIX}/lib/libshm.a /tmp/shm_stub.o

# coroutine sigsetjmp shim — Bionic's sigsetjmp uses PAC instructions (paciasp /
Expand All @@ -294,7 +280,7 @@ RUN printf '#include <sys/syscall.h>\n#include <unistd.h>\n#include <errno.h>\n#
# no syscalls, no stack-protector wrapping. ~22 doublewords -> fits in sigjmp_buf.
RUN printf '#ifndef PODROID_QEMU_JMP_H\n#define PODROID_QEMU_JMP_H\n#include <setjmp.h>\nextern int _qemu_setjmp(sigjmp_buf);\n__attribute__((noreturn)) extern void _qemu_longjmp(sigjmp_buf, int);\n#endif\n' > /opt/qemu_jmp.h \
&& printf '.text\n.global _qemu_setjmp\n.type _qemu_setjmp,%%function\n_qemu_setjmp:\nstp x19,x20,[x0,#0]\nstp x21,x22,[x0,#16]\nstp x23,x24,[x0,#32]\nstp x25,x26,[x0,#48]\nstp x27,x28,[x0,#64]\nstp x29,x30,[x0,#80]\nmov x9,sp\nstr x9,[x0,#96]\nstp d8,d9,[x0,#104]\nstp d10,d11,[x0,#120]\nstp d12,d13,[x0,#136]\nstp d14,d15,[x0,#152]\nmov w0,#0\nret\n.size _qemu_setjmp,.-_qemu_setjmp\n.global _qemu_longjmp\n.type _qemu_longjmp,%%function\n_qemu_longjmp:\nldp x19,x20,[x0,#0]\nldp x21,x22,[x0,#16]\nldp x23,x24,[x0,#32]\nldp x25,x26,[x0,#48]\nldp x27,x28,[x0,#64]\nldp x29,x30,[x0,#80]\nldr x9,[x0,#96]\nmov sp,x9\nldp d8,d9,[x0,#104]\nldp d10,d11,[x0,#120]\nldp d12,d13,[x0,#136]\nldp d14,d15,[x0,#152]\ncmp w1,#0\ncsinc w0,w1,wzr,ne\nbr x30\n.size _qemu_longjmp,.-_qemu_longjmp\n.section .note.GNU-stack,"",%%progbits\n' > /tmp/qemu_jmp.S \
&& ${CC} --sysroot=${LLVM}/sysroot -target aarch64-linux-android28 -c /tmp/qemu_jmp.S -o /tmp/qemu_jmp.o \
&& ${CC} --sysroot=${LLVM}/sysroot -target aarch64-linux-android26 -c /tmp/qemu_jmp.S -o /tmp/qemu_jmp.o \
&& ${AR} rcs ${PREFIX}/lib/libqemujmp.a /tmp/qemu_jmp.o

# Patch coroutine-ucontext.c to call our PAC-free shim instead of libc's sigsetjmp/siglongjmp.
Expand All @@ -314,11 +300,11 @@ RUN cd ${QEMU_DIR} && ./configure --cc="${CC}" --cross-prefix="${LLVM}/bin/llvm-

# Bridge
COPY podroid-bridge.c /tmp/podroid-bridge.c
RUN ${CC} --sysroot=${LLVM}/sysroot -target aarch64-linux-android28 -fPIE -pie -Wl,-z,max-page-size=16384 /tmp/podroid-bridge.c -o /opt/qemu-out/libpodroid-bridge.so
RUN ${CC} --sysroot=${LLVM}/sysroot -target aarch64-linux-android26 -fPIE -pie -Wl,-z,max-page-size=16384 /tmp/podroid-bridge.c -o /opt/qemu-out/libpodroid-bridge.so

# Launcher (PR_SET_PDEATHSIG wrapper for QEMU — see podroid-launcher.c)
COPY podroid-launcher.c /tmp/podroid-launcher.c
RUN ${CC} --sysroot=${LLVM}/sysroot -target aarch64-linux-android28 -fPIE -pie -Wl,-z,max-page-size=16384 /tmp/podroid-launcher.c -o /opt/qemu-out/libpodroid-launcher.so
RUN ${CC} --sysroot=${LLVM}/sysroot -target aarch64-linux-android26 -fPIE -pie -Wl,-z,max-page-size=16384 /tmp/podroid-launcher.c -o /opt/qemu-out/libpodroid-launcher.so

# Soname fix
RUN cp /opt/qemu-out/bin/qemu-system-aarch64 /opt/qemu-out/libqemu-system-aarch64.so \
Expand Down
Loading