apply plugin: 'com.android.application' // Paths written by build-image/prepare-android-project.sh (called by // build-apk.sh, or standalone with --host-paths to prep for a native // build in Android Studio - see README) since they're only known at // prep/build time, not something a checked-in build.gradle can hardcode. // engineDir is the scratch, patched copy of engine/; gl4esDir is the // scratch, patched copy of android/gl4es-src/ (built from source as a // CMake subdirectory of engine/'s own native build below, not prebuilt); // prebuiltDir is the Android SDL2/SDL2_mixer/fluidsynth-lite/openxr // prebuilts - all point into this container's own paths for // build-apk.sh's own gradlew call, or host-resolvable paths (plus a // host-side export of prebuiltDir, since /opt/prebuilt/android only // exists in the image) when prepared with --host-paths for Android // Studio. def engineProps = new Properties() file("${projectDir}/../engine.properties").withInputStream { engineProps.load(it) } def engineDir = engineProps.getProperty('engineDir') if (engineDir == null) { throw new GradleException("android/engine.properties is missing 'engineDir' - run via build-apk.sh or prepare-android-project.sh, not gradlew directly") } def gl4esDir = engineProps.getProperty('gl4esDir') if (gl4esDir == null) { throw new GradleException("android/engine.properties is missing 'gl4esDir' - run via build-apk.sh or prepare-android-project.sh, not gradlew directly") } def prebuiltDir = engineProps.getProperty('prebuiltDir') if (prebuiltDir == null) { throw new GradleException("android/engine.properties is missing 'prebuiltDir' - run via build-apk.sh or prepare-android-project.sh, not gradlew directly") } // Set by build-apk.sh (see build-image/version.sh, shared with the // desktop tarball's versioning) via -PquestshockVersionName/Code. Default // here only covers a direct, unsupported `gradlew` invocation. def questshockVersionName = project.hasProperty('questshockVersionName') ? project.property('questshockVersionName') : '0.0.0-dev' def questshockVersionCode = project.hasProperty('questshockVersionCode') ? project.property('questshockVersionCode').toInteger() : 1 // Auto-refreshes the staged, patched engine/ and gl4es-src/ copies and // prebuilt libraries (see build-image/prepare-android-project.sh // --host-paths, and the engineDir/gl4esDir/prebuiltDir properties read // above) so a plain Android Studio build/run can never silently compile // against a stale scratch copy after engine/, android/engine-patches/, // android/gl4es-src/, or android/gl4es-patches/ change - previously a // manual, easy to forget re-run. inputs/outputs are declared so Gradle // skips the (Docker-invoking, not free) step entirely when nothing // relevant actually changed, keeping pure-Java edit/run cycles fast. // // This step only re-stages whatever is already baked into the build-image // (it re-runs prepare-android-project.sh inside the existing image, which // is fast - no compilation happens here) - engine/ and gl4es-src/ are both // then compiled from these scratch copies by the externalNativeBuild CMake // configure below (see android/engine-patches/02-android-opengl-es.patch's // add_subdirectory of gl4esDir), so Gradle's own incremental CMake/Ninja // build only recompiles what actually changed, same as any other native // source edit. This task does NOT rebuild the image itself: if you changed // build-image/Dockerfile (a real toolchain/dependency change, not // engine-patches/ or gl4es-patches/, which this task re-stages on its own), // you still need to run ./build-image.sh yourself first (a slow, deliberate // step - see the memory on build-image versioning) so the image actually // contains your changes before this task re-stages them. def repoRoot = file("${projectDir}/../..") def stageEngineTask = tasks.register("stageEngine", Exec) { group = "build setup" description = "Refreshes the patched engine/ scratch copy and prebuilt libraries for a native Android Studio build (build-image/prepare-android-project.sh --host-paths)." workingDir repoRoot commandLine "./run-image.sh", "bash", "build-image/prepare-android-project.sh", "--host-paths" // /opt/prebuilt/android only ever exists inside the build-image // container itself (baked in at image-build time, never on the host - // see build-image/Dockerfile). Its presence means this build is // build-apk.sh's own gradlew call, running INSIDE that container, which // already staged everything itself in container mode before invoking // gradlew - re-running this task there would try to `docker run` from // inside a container with no docker socket, breaking make apk/CI. Only // a genuine host-side Android Studio build (where this path is absent) // needs this task. onlyIf { !file('/opt/prebuilt/android').isDirectory() } inputs.dir("${repoRoot}/engine") inputs.dir("${projectDir}/../engine-patches") inputs.dir("${projectDir}/../gl4es-src") inputs.dir("${projectDir}/../gl4es-patches") inputs.file("${repoRoot}/build-image/Dockerfile") outputs.dir("${repoRoot}/build/android-engine") outputs.dir("${repoRoot}/build/android-gl4es") outputs.dir("${repoRoot}/build/android-prebuilt") outputs.file("${projectDir}/../engine.properties") } // preBuild is what every variant's compile/native-build tasks already // transitively depend on, regardless of AGP version's exact CMake task // naming - the simplest reliable hook to run before any of them. afterEvaluate { tasks.named("preBuild").configure { dependsOn stageEngineTask } } android { namespace "de.ladkau.questshock" // compileSdk/buildToolsVersion/ndkVersion must all match what // build-image/Dockerfile actually installs - otherwise AGP defaults to // whatever version it itself prefers and tries to download it into the // SDK dir at build time, which isn't writable. compileSdk 34 buildToolsVersion "34.0.0" ndkVersion "26.1.10909125" defaultConfig { applicationId "de.ladkau.questshock" // 29, not compileSdk's 34, is deliberate - see build-image/Dockerfile's // comment on ANDROID_PLATFORM_VERSION: paired with // requestLegacyExternalStorage in the manifest, this keeps // /sdcard/questshock/ a plain, unrestricted shared folder instead of // Android 11+ scoped storage, on the Quest 2/3/3S/Pro headsets this // targets - not just whatever's the current API level today. minSdkVersion 24 targetSdkVersion 29 versionCode questshockVersionCode versionName questshockVersionName externalNativeBuild { cmake { // engine/CMakeLists.txt's own dependency-selection options // (see engine/CMakeLists.txt near the top): SDL2 via // find_package (our Android SDL2 build installs a real CMake // config, unlike the desktop build's older bundled copy); // SDL2_mixer and FluidSynth via the same build_ext/ BUNDLED // convention the desktop build already uses (populated in the // scratch engine copy by prepare-android-project.sh from // prebuiltDir). ANDROID_GL4ES_DIR is read by // android/engine-patches/02-android-opengl-es.patch, which // add_subdirectory()'s it to compile gl4es from source as // part of this same CMake configure, instead of linking a // prebuilt .so - so gl4es-patches/ changes just need a normal // rebuild here, not a build-image rebuild. // The CMAKE_FIND_ROOT_PATH* overrides below are needed // because the NDK toolchain file restricts find_package/ // find_path/find_library to its own sysroot by default, // which would otherwise miss our custom-installed SDL2 (see // build-image/Dockerfile's SDL2_mixer build, which hit the // exact same thing). // -Wl,-z,max-page-size=16384: NDK 26 doesn't 16 KB-align ELF // LOAD segments by default (only automatic in NDK 28+) - see // build-image/Dockerfile's ANDROID_16KB_LDFLAGS, which does // the same for the prebuilt SDL2/SDL2_mixer/fluidsynth-lite // .so's this links against (gl4es is compiled here with the // same flag passed straight through, see its add_subdirectory // above). // ANDROID_EXTRA_SOURCES is a semicolon-separated CMake list, // not shell-split - AGP passes each "arguments" string // straight through as one argv element to cmake, so the // ';' below survives intact. ANDROID_EXTRA_INCLUDE_DIRS lets // engine/'s own OpenGL.cc find "xr_session.h" (see // android/engine-patches/10-android-openxr-cmake.patch, // which adds it to include_directories()). // USE_ANDROID_LOG/STATICLIB used to be passed to gl4es's own, // separate cmake invocation in build-image/Dockerfile - now // that it's add_subdirectory()'d into this same configure, // they need to be set here instead, or gl4es silently falls // back to stdio logging, which never reaches logcat. ANDROID // itself doesn't need setting here - the NDK toolchain file // already defines it before any CMakeLists.txt runs. arguments "-DENABLE_SDL2=ON", "-DENABLE_SOUND=BUNDLED", "-DENABLE_FLUIDSYNTH=BUNDLED", \ "-DANDROID_PREBUILT_DIR=${prebuiltDir}", \ "-DANDROID_GL4ES_DIR=${gl4esDir}", \ "-DUSE_ANDROID_LOG=ON", "-DSTATICLIB=OFF", \ "-DANDROID_EXTRA_INCLUDE_DIRS=${projectDir}/src/main/cpp", \ "-DCMAKE_PREFIX_PATH=${prebuiltDir}/sdl2", \ "-DCMAKE_FIND_ROOT_PATH=${prebuiltDir}/sdl2", \ "-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH", \ "-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=BOTH", \ "-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH", \ "-DCMAKE_SHARED_LINKER_FLAGS=-Wl,-z,max-page-size=16384", \ "-DANDROID_EXTRA_SOURCES=${projectDir}/src/main/cpp/questshock_native.c;${projectDir}/src/main/cpp/xr_session.c;${projectDir}/src/main/cpp/xr_input.c;${projectDir}/src/main/cpp/xr_overlay.c;${projectDir}/src/main/cpp/xr_swapchain.c;${projectDir}/src/main/cpp/xr_mouse.c" abiFilters 'arm64-v8a' } } } externalNativeBuild { cmake { path file("${engineDir}/CMakeLists.txt") } } buildTypes { release { minifyEnabled false } } lint { abortOnError false } } dependencies { // ActivityCompat.checkSelfPermission/requestPermissions in // QuestShockActivity - the plain android.app.Activity APIs work the // same way without androidx, but ActivityCompat is the standard, // documented way to request runtime permissions safely. implementation 'androidx.core:core:1.12.0' }