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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ __pycache__/

# Android/Gradle
build/
!/docker/build/
!/docker/build/**
.gradle/
local.properties
*.iml
Expand Down
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -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

Expand Down
3 changes: 3 additions & 0 deletions docker/build/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Whitelist: o único arquivo do contexto que a imagem usa.
*
!entrypoint.sh
185 changes: 185 additions & 0 deletions docker/build/Dockerfile
Original file line number Diff line number Diff line change
@@ -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 <<EOT
set -eux
curl -fsSL -o /tmp/cmdline-tools.zip \
"https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS_VERSION}_latest.zip"
echo "${CMDLINE_TOOLS_SHA256} /tmp/cmdline-tools.zip" | sha256sum -c -
mkdir -p "${ANDROID_HOME}/cmdline-tools"
unzip -q /tmp/cmdline-tools.zip -d "${ANDROID_HOME}/cmdline-tools"
mv "${ANDROID_HOME}/cmdline-tools/cmdline-tools" "${ANDROID_HOME}/cmdline-tools/latest"
rm /tmp/cmdline-tools.zip
yes | "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" --licenses > /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 <<EOT
set -eux
git clone https://github.com/Javernaut/ffmpeg-android-maker.git /opt/ffmpeg-android-maker
cd /opt/ffmpeg-android-maker
git checkout "${FFMPEG_MAKER_REF}"
./ffmpeg-android-maker.sh --target-abis=arm64-v8a --android-api-level=26
# Só o resultado: sem fontes, .git nem o script do maker.
mkdir -p /out/build/ffmpeg /out/output/include
cp -a build/ffmpeg/arm64-v8a /out/build/ffmpeg/arm64-v8a
# scripts/build.sh referencia output/include/arm64-v8a (symlink relativo).
ln -s ../../build/ffmpeg/arm64-v8a/include /out/output/include/arm64-v8a
EOT

# ── rust: toolchain fixo + cargo-ndk ─────────────────────────────────────────
FROM base AS rust
ARG RUSTUP_VERSION=1.28.2
ARG RUSTUP_INIT_SHA256=20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c
ARG RUST_VERSION=1.97.1
ARG CARGO_NDK_VERSION=4.1.2
ENV RUSTUP_HOME=/opt/rustup \
CARGO_HOME=/opt/cargo \
PATH=/opt/cargo/bin:${PATH}

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 \
curl

RUN <<EOT
set -eux
curl -fsSL -o /tmp/rustup-init \
"https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/x86_64-unknown-linux-gnu/rustup-init"
echo "${RUSTUP_INIT_SHA256} /tmp/rustup-init" | sha256sum -c -
chmod +x /tmp/rustup-init
/tmp/rustup-init -y --no-modify-path --profile minimal \
--default-toolchain "${RUST_VERSION}" --target aarch64-linux-android
rm /tmp/rustup-init
EOT

RUN --mount=type=cache,target=/opt/cargo/registry \
--mount=type=cache,target=/opt/cargo/git \
cargo install cargo-ndk --locked --version "${CARGO_NDK_VERSION}" \
&& chmod -R a+rX /opt/rustup /opt/cargo

# ── final ────────────────────────────────────────────────────────────────────
FROM base AS final
ARG NDK_VERSION=26.3.11579264

LABEL org.opencontainers.image.title="tucaVR builder" \
org.opencontainers.image.description="Toolchain de build do APK debug do tucaVR" \
org.opencontainers.image.source="https://github.com/BGLuis/tucaVR"

# build-essential: linker do host para build.rs/proc-macros. libclang-dev: bindgen.
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 \
git \
libclang-dev \
openjdk-17-jdk-headless \
pkg-config

COPY --link --from=sdk /opt/android-sdk /opt/android-sdk
COPY --link --from=rust /opt/rustup /opt/rustup
COPY --link --from=rust /opt/cargo/bin /opt/cargo/bin
COPY --link --from=ffmpeg /out/ /opt/ffmpeg-android-maker/

# Caches e HOME num caminho gravável por qualquer UID (o container roda com o
# UID do host via --user). Volumes nomeados herdam estas permissões.
RUN mkdir -p -m 1777 /cache/gradle /cache/cargo /cache/android /cache/home

ENV ANDROID_HOME=/opt/android-sdk \
ANDROID_SDK_ROOT=/opt/android-sdk \
ANDROID_NDK_HOME=/opt/android-sdk/ndk/${NDK_VERSION} \
ANDROID_NDK_ROOT=/opt/android-sdk/ndk/${NDK_VERSION} \
ANDROID_USER_HOME=/cache/android \
FFMPEG_MAKER_DIR=/opt/ffmpeg-android-maker \
RUSTUP_HOME=/opt/rustup \
CARGO_HOME=/cache/cargo \
GRADLE_USER_HOME=/cache/gradle \
HOME=/cache/home \
PATH=/opt/cargo/bin:${PATH} \
GIT_CONFIG_COUNT=1 \
GIT_CONFIG_KEY_0=safe.directory \
GIT_CONFIG_VALUE_0=/project

WORKDIR /project
COPY --chmod=755 entrypoint.sh /usr/local/bin/entrypoint.sh

# UID estático e não-root por padrão; scripts/docker-build.sh sobrescreve com
# o UID/GID do host. Sem HEALTHCHECK: é um container de build, não um serviço.
USER 10001:10001
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
92 changes: 92 additions & 0 deletions docker/build/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# tucaVR — Docker Build

Build do APK debug dentro de um container, sem instalar Android SDK, NDK, Rust
ou FFmpeg no host. O container delega para `scripts/build.sh`, o mesmo pipeline
do build local e do CI.

## Pré-requisitos

1. **Docker** (ou Podman) funcionando: <https://docs.docker.com/engine/install/>.
2. **Meta OpenXR Mobile SDK** em `sdk/meta-openxr-sdk/` (download manual, exige
aceite de licença):
- <https://developers.meta.com/horizon/downloads/package/oculus-openxr-mobile-sdk/>
- 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`.
38 changes: 38 additions & 0 deletions docker/build/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -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 "$@"
14 changes: 11 additions & 3 deletions scripts/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 ..

Expand Down
Loading
Loading