diff --git a/.gitignore b/.gitignore index dbf320b..4a94f81 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ __pycache__/ # Android/Gradle build/ +!/docker/build/ +!/docker/build/** .gradle/ local.properties *.iml diff --git a/CLAUDE.md b/CLAUDE.md index 9929ab9..9ab3990 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,6 +43,11 @@ make build # same thing make deploy # build + adb install ``` +Containerized build (no SDK/NDK/Rust/FFmpeg on the host; runs `scripts/build.sh` inside `docker/build/`, still needs the Meta SDK under `sdk/`; does not work from a `git worktree`): +```bash +./scripts/docker-build.sh # or: make docker-build +``` + Rust alone (must use `cargo ndk`, not plain `cargo build`, for anything touching `core`/`audio`/`bridge`): ```bash cd rust && cargo ndk -t aarch64-linux-android -P 26 -o ../app/src/main/jniLibs build --release diff --git a/Makefile b/Makefile index 200a09b..adbf3ab 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,14 @@ -.PHONY: all build install deploy test +.PHONY: all build docker-build install deploy test all: deploy build: ./scripts/build.sh +# Mesmo pipeline do `build`, dentro de um container (nada instalado no host). +docker-build: + ./scripts/docker-build.sh + install: adb install -r app/build/outputs/apk/debug/app-debug.apk diff --git a/docker/build/.dockerignore b/docker/build/.dockerignore new file mode 100644 index 0000000..65ea728 --- /dev/null +++ b/docker/build/.dockerignore @@ -0,0 +1,3 @@ +# Whitelist: o único arquivo do contexto que a imagem usa. +* +!entrypoint.sh diff --git a/docker/build/Dockerfile b/docker/build/Dockerfile new file mode 100644 index 0000000..309b8a7 --- /dev/null +++ b/docker/build/Dockerfile @@ -0,0 +1,185 @@ +# syntax=docker/dockerfile:1 +# ============================================================================= +# tucaVR — imagem de build do APK debug +# +# Estágios independentes (o BuildKit constrói sdk, ffmpeg e rust em paralelo): +# sdk -> Android SDK 34, NDK 26.3, CMake 3.22.1, build-tools +# ffmpeg -> FFmpeg cross-compilado para arm64-v8a (ffmpeg-android-maker) +# rust -> Rust fixo + target aarch64-linux-android + cargo-ndk +# final -> só o que o build precisa em runtime (sem nasm/yasm/curl/wget, +# sem .git nem fontes do FFmpeg) +# +# O código-fonte e o Meta OpenXR SDK são montados em /project por volume. +# O container roda como usuário não-root; use scripts/docker-build.sh, que +# passa --user com o UID/GID do host (arquivos gerados ficam seus, sem chown). +# ============================================================================= + +# Pins. Atualize digest/versões de propósito, nunca "latest". +ARG UBUNTU_IMAGE=ubuntu:22.04@sha256:b8b6ee6aa931ecd9d0d952abc34dc0e5f7c6a30c6bb71b079fe399fde0329c02 + +FROM ${UBUNTU_IMAGE} AS base +ENV DEBIAN_FRONTEND=noninteractive \ + LANG=C.UTF-8 \ + LC_ALL=C.UTF-8 +# Sem isso o docker-clean apaga o cache do apt e o cache mount fica vazio. +RUN rm -f /etc/apt/apt.conf.d/docker-clean + +# ── sdk: Android SDK + NDK + CMake ─────────────────────────────────────────── +FROM base AS sdk +ARG CMDLINE_TOOLS_VERSION=11076708 +ARG CMDLINE_TOOLS_SHA256=2d2d50857e4eb553af5a6dc3ad507a17adf43d115264b1afc116f95c92e5e258 +ARG NDK_VERSION=26.3.11579264 +ENV ANDROID_HOME=/opt/android-sdk + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + openjdk-17-jdk-headless \ + unzip + +RUN < /dev/null + "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" \ + "platforms;android-34" \ + "build-tools;34.0.0" \ + "ndk;${NDK_VERSION}" \ + "cmake;3.22.1" + # Nada aqui escreve no SDK depois; deixa legível para qualquer UID. + chmod -R a+rX "${ANDROID_HOME}" +EOT + +# ── ffmpeg: cross-compile para arm64-v8a ───────────────────────────────────── +FROM base AS ffmpeg +# Mesmo commit de scripts/setup-deps.sh (FFMPEG_MAKER_COMMIT). +ARG FFMPEG_MAKER_REF=dd72b161ae5c759fd25a5cab971a3ff710f0bdba +ARG NDK_VERSION=26.3.11579264 +ENV ANDROID_SDK_HOME=/opt/android-sdk \ + ANDROID_NDK_HOME=/opt/android-sdk/ndk/${NDK_VERSION} + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + cmake \ + curl \ + git \ + nasm \ + ninja-build \ + pkg-config \ + python3 \ + unzip \ + wget \ + yasm + +# Só o NDK: o resto do SDK não participa do build do FFmpeg. +COPY --from=sdk /opt/android-sdk/ndk /opt/android-sdk/ndk + +# O prefixo dos .pc gerados é /opt/ffmpeg-android-maker/...; o estágio final +# copia para o mesmo caminho para o pkg-config continuar resolvendo. +RUN <. +2. **Meta OpenXR Mobile SDK** em `sdk/meta-openxr-sdk/` (download manual, exige + aceite de licença): + - + - deve existir `sdk/meta-openxr-sdk/Samples/SampleXrFramework/`. +3. Um **checkout normal ou clone** do repositório. `git worktree` não funciona: + o `.git` de um worktree aponta para um caminho que não existe no container + (o entrypoint detecta e avisa). + +## Uso + +Da raiz do projeto: + +```bash +./scripts/docker-build.sh # constrói a imagem (cache) e compila o APK +./scripts/docker-build.sh --rebuild # refaz a imagem do zero (--no-cache --pull) +./scripts/docker-build.sh --shell # shell interativo no container +make docker-build # atalho para o primeiro +``` + +O APK sai em `app/build/outputs/apk/debug/app-debug.apk`. + +`APP_VERSION_NAME` e `APP_VERSION_CODE`, se definidos no host, são repassados ao +Gradle. `DOCKER=podman ./scripts/docker-build.sh` usa outro runtime (o caminho +com Podman rootless não foi testado). + +## Como funciona + +- **Multi-stage**: `sdk`, `ffmpeg` e `rust` são independentes (o BuildKit os + constrói em paralelo). A imagem final leva só o necessário para compilar: sem + `nasm`/`yasm`/`curl`/`wget`, sem `.git` do `ffmpeg-android-maker` e sem fontes + do FFmpeg. +- **Não-root**: o container roda com o UID/GID do host (`--user`), então tudo o + que ele gera já nasce seu, sem `chown` e sem arquivos de root no projeto. + Também usa `--cap-drop ALL` e `no-new-privileges`. Sem `--user`, a imagem usa + `10001:10001` (que não escreve no projeto montado). +- **Nada é escrito fora dos caches e do projeto**: o FFmpeg pré-compilado fica em + `/opt/ffmpeg-android-maker` e o `build.sh` o encontra por `FFMPEG_MAKER_DIR`; não há + symlink dentro do seu projeto. +- **Reprodutível**: base Ubuntu por digest, Rust, `cargo-ndk`, `rustup-init` e + `commandlinetools` fixados (os dois últimos com SHA-256), FFmpeg no mesmo commit + de `scripts/setup-deps.sh`. Atualize os `ARG` do Dockerfile de propósito. + +## Conteúdo da imagem + +| Componente | Versão | +|------------|--------| +| Ubuntu | 22.04 (por digest) | +| JDK | 17 (OpenJDK headless) | +| Android SDK | Platform 34, build-tools 34.0.0 | +| Android NDK | 26.3.11579264 | +| CMake | 3.22.1 (via SDK) | +| Rust | 1.97.1 + `aarch64-linux-android` | +| cargo-ndk | 4.1.2 | +| FFmpeg | `arm64-v8a`, API 26 | + +## Caches + +Volumes nomeados persistem entre builds: + +| Volume | Conteúdo | +|--------|----------| +| `tucavr-gradle-cache` | Dependências e cache do Gradle (`/cache/gradle`) | +| `tucavr-cargo-cache` | Registry e git de crates (`/cache/cargo`) | + +Para limpar: `docker volume rm tucavr-gradle-cache tucavr-cargo-cache`. + +## Uso manual (sem o script) + +```bash +docker build -t tucavr-builder docker/build/ +docker run --rm --init --user "$(id -u):$(id -g)" \ + --cap-drop ALL --security-opt no-new-privileges:true \ + -v "$(pwd)":/project \ + -v tucavr-gradle-cache:/cache/gradle \ + -v tucavr-cargo-cache:/cache/cargo \ + tucavr-builder +``` + +## Solução de problemas + +- `/project é um git worktree`: use um clone completo (veja Pré-requisitos). +- `Meta OpenXR SDK não encontrado`: veja Pré-requisitos, item 2. +- Cache corrompido ou lento: apague os volumes acima e rode com `--rebuild`. diff --git a/docker/build/entrypoint.sh b/docker/build/entrypoint.sh new file mode 100755 index 0000000..750f1a4 --- /dev/null +++ b/docker/build/entrypoint.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# ============================================================================= +# tucaVR — Entrypoint do container de build +# +# Só valida o ambiente e delega para scripts/build.sh, a única fonte de verdade +# do pipeline (cargo ndk -> .so do FFmpeg -> gradle). O container roda com o +# UID do host, então nada aqui precisa de chown nem toca em arquivos do projeto. +# ============================================================================= +set -euo pipefail + +die() { + echo "❌ $1" >&2 + shift + for line in "$@"; do echo " $line" >&2; done + exit 1 +} + +cd /project + +[ -f build.gradle.kts ] || die "Código-fonte do projeto não encontrado em /project" \ + "Monte o projeto com: -v \"\$(pwd)\":/project" + +[ -d sdk/meta-openxr-sdk/Samples/SampleXrFramework ] || die "Meta OpenXR SDK não encontrado em sdk/meta-openxr-sdk/" \ + "Esse SDK exige download manual com aceite de licença:" \ + "https://developers.meta.com/horizon/downloads/package/oculus-openxr-mobile-sdk/" + +# Num git worktree o .git é um arquivo que aponta para um caminho do host, +# inexistente aqui; o Gradle usa git e falharia tarde e com erro confuso. +[ ! -f .git ] || die "/project é um git worktree (.git é um arquivo)" \ + "O caminho gitdir dele não existe dentro do container." \ + "Use o checkout principal ou um clone completo (git clone)." + +[ -d "${FFMPEG_MAKER_DIR}/build/ffmpeg/arm64-v8a/lib" ] || die "FFmpeg pré-compilado ausente em ${FFMPEG_MAKER_DIR}" \ + "A imagem está corrompida; refaça com: scripts/docker-build.sh --rebuild" + +echo "📋 NDK=${ANDROID_NDK_HOME} FFMPEG_MAKER_DIR=${FFMPEG_MAKER_DIR} UID=$(id -u):$(id -g)" + +exec ./scripts/build.sh "$@" diff --git a/scripts/build.sh b/scripts/build.sh index bce22a0..924ef2a 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -11,15 +11,23 @@ echo "🦀 Compilando Rust Core (aarch64-linux-android)..." export ANDROID_NDK_HOME=${ANDROID_NDK_HOME:-$ANDROID_HOME/ndk/26.3.11579264} export ANDROID_NDK_ROOT=$ANDROID_NDK_HOME export PKG_CONFIG_ALLOW_CROSS=1 -export PKG_CONFIG_PATH="$ROOT_DIR/ffmpeg-android-maker/build/ffmpeg/arm64-v8a/lib/pkgconfig" -export BINDGEN_EXTRA_CLANG_ARGS="-I$ROOT_DIR/ffmpeg-android-maker/output/include/arm64-v8a -I$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include --sysroot=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/sysroot" +# FFMPEG_MAKER_DIR permite apontar para um FFmpeg pré-compilado fora do projeto +# (é o que o container de docker/build faz); por padrão usa o de setup-deps.sh. +FFMPEG_MAKER_DIR="${FFMPEG_MAKER_DIR:-$ROOT_DIR/ffmpeg-android-maker}" +if ! ls "$FFMPEG_MAKER_DIR"/build/ffmpeg/arm64-v8a/lib/*.so > /dev/null 2>&1; then + echo "❌ FFmpeg para arm64-v8a não encontrado em $FFMPEG_MAKER_DIR" >&2 + echo " Rode ./scripts/setup-deps.sh e compile o FFmpeg (veja o README)." >&2 + exit 1 +fi +export PKG_CONFIG_PATH="$FFMPEG_MAKER_DIR/build/ffmpeg/arm64-v8a/lib/pkgconfig" +export BINDGEN_EXTRA_CLANG_ARGS="-I$FFMPEG_MAKER_DIR/output/include/arm64-v8a -I$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include --sysroot=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/sysroot" cd rust # cargo ndk copia automaticamente os .so se usarmos o -o cargo ndk -t aarch64-linux-android -P 26 -o ../app/src/main/jniLibs build --release # Copy FFmpeg shared libraries -cp ../ffmpeg-android-maker/build/ffmpeg/arm64-v8a/lib/*.so ../app/src/main/jniLibs/arm64-v8a/ || true +cp "$FFMPEG_MAKER_DIR"/build/ffmpeg/arm64-v8a/lib/*.so ../app/src/main/jniLibs/arm64-v8a/ cd .. diff --git a/scripts/docker-build.sh b/scripts/docker-build.sh new file mode 100755 index 0000000..01ebbc5 --- /dev/null +++ b/scripts/docker-build.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# ============================================================================= +# tucaVR — Build do APK debug dentro de um container (sem instalar SDK/NDK/Rust +# no host). +# +# Uso: +# ./scripts/docker-build.sh # constrói a imagem (cache) e compila o APK +# ./scripts/docker-build.sh --rebuild # refaz a imagem do zero (--no-cache --pull) +# ./scripts/docker-build.sh --shell # shell interativo no container +# +# Variáveis: APP_VERSION_NAME, APP_VERSION_CODE (repassadas ao Gradle) e +# DOCKER (padrão: docker; ex.: DOCKER=podman). +# ============================================================================= +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +DOCKER="${DOCKER:-docker}" +IMAGE_NAME="tucavr-builder" +DOCKERFILE_DIR="docker/build" + +FORCE_REBUILD=false +INTERACTIVE_SHELL=false + +for arg in "$@"; do + case "$arg" in + --rebuild) FORCE_REBUILD=true ;; + --shell) INTERACTIVE_SHELL=true ;; + --help|-h) + sed -n '2,13p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' + exit 0 + ;; + *) + echo "❌ Argumento desconhecido: $arg (use --help)" >&2 + exit 1 + ;; + esac +done + +if ! command -v "$DOCKER" &> /dev/null; then + echo "❌ '$DOCKER' não encontrado. Instale o Docker: https://docs.docker.com/engine/install/" >&2 + exit 1 +fi + +# A checagem do Meta OpenXR SDK fica no entrypoint (uma só fonte); aqui só +# evitamos esperar o build da imagem para descobrir que ele falta. +if [ ! -d "sdk/meta-openxr-sdk/Samples/SampleXrFramework" ]; then + echo "⚠️ Meta OpenXR SDK não encontrado em sdk/meta-openxr-sdk/" >&2 + echo " Baixe em https://developers.meta.com/horizon/downloads/package/oculus-openxr-mobile-sdk/" >&2 + echo " e extraia para sdk/meta-openxr-sdk/." >&2 + exit 1 +fi + +# Sempre roda o build da imagem: sem mudanças é só cache (segundos), e mudanças +# no Dockerfile/entrypoint nunca ficam com imagem velha. +BUILD_ARGS=() +if $FORCE_REBUILD; then + BUILD_ARGS+=(--no-cache --pull) +fi +echo "🐳 Preparando a imagem '$IMAGE_NAME' (a primeira vez leva vários minutos)..." +"$DOCKER" build "${BUILD_ARGS[@]}" -t "$IMAGE_NAME" "$DOCKERFILE_DIR" + +# Roda com o UID/GID do host: arquivos gerados já nascem seus, sem chown/root. +RUN_ARGS=( + --rm + --init + --user "$(id -u):$(id -g)" + --cap-drop ALL + --security-opt no-new-privileges:true + -v "$ROOT_DIR:/project" + # Caches persistentes para builds incrementais + -v tucavr-gradle-cache:/cache/gradle + -v tucavr-cargo-cache:/cache/cargo + -e APP_VERSION_NAME + -e APP_VERSION_CODE +) +# Podman rootless: mantém o UID do host dentro do container. +if [[ "$(basename "$DOCKER")" == podman* ]]; then + RUN_ARGS+=(--userns=keep-id) +fi + +if $INTERACTIVE_SHELL; then + echo "🐚 Shell no container (projeto em /project, FFmpeg em /opt/ffmpeg-android-maker)" + "$DOCKER" run -it "${RUN_ARGS[@]}" --entrypoint /bin/bash "$IMAGE_NAME" +else + echo "🚀 Compilando o APK no container..." + "$DOCKER" run "${RUN_ARGS[@]}" "$IMAGE_NAME" + echo "📦 APK: app/build/outputs/apk/debug/app-debug.apk" +fi