From f87719833ada79be7003269d8cbd5c276f4d283d Mon Sep 17 00:00:00 2001 From: Linkumori <142691487+subham8907@users.noreply.github.com> Date: Thu, 30 Apr 2026 19:01:19 +0530 Subject: [PATCH] Add automatic Android NDK provisioning --- README.md | 3 + build.gradle | 7 +- .../src/main/groovy/BuildNativeTask.groovy | 20 ++- .../main/groovy/DownloadAndroidNdkTask.groovy | 117 ++++++++++++++++++ gradle.properties | 1 + 5 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 buildSrc/src/main/groovy/DownloadAndroidNdkTask.groovy diff --git a/README.md b/README.md index c0345ba3..ebd9eeec 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,9 @@ class Main { - aarch64-linux - aarch64-linux-android +# Android native builds +Android native builds use the Android NDK compiler. If `ANDROID_NDK_HOME`, `ANDROID_NDK_ROOT`, `ANDROID_HOME`, or `ANDROID_SDK_ROOT` points to an installed NDK, the build uses it. Otherwise, Gradle downloads the NDK version configured by `androidNdkVersion` in `gradle.properties` into the root `build/android-ndk` directory. + # Developers: How to Add a Parser To add a new language parser to this project, we provide a code generation task that handles most of the boilerplate. This is also how you can add an "unofficial" or community parser. diff --git a/build.gradle b/build.gradle index 0f3979af..d98769fe 100644 --- a/build.gradle +++ b/build.gradle @@ -114,6 +114,9 @@ subprojects { tasks.register('buildNative', BuildNativeTask) { zigExe = rootProject.downloadZig.zigExe dependsOn downloadSource, rootProject.downloadZig + if (((String) rootProject.properties.get("treeSitterTargets")).contains("android")) { + dependsOn rootProject.downloadAndroidNdk + } } tasks.withType(Javadoc) { @@ -139,6 +142,8 @@ tasks.register('downloadZig', DownloadZigTask){ dependsOn downloadMiniSign } +tasks.register('downloadAndroidNdk', DownloadAndroidNdkTask) + tasks.register("gen", GenTask) { group = "build setup" description = "Generate sub project for tree sitter parser" @@ -251,4 +256,4 @@ tasks.register("postPublish") { } println "Published to Portal with response body:\n${resp.body()}" } -} \ No newline at end of file +} diff --git a/buildSrc/src/main/groovy/BuildNativeTask.groovy b/buildSrc/src/main/groovy/BuildNativeTask.groovy index 51e9ceae..13fb943e 100644 --- a/buildSrc/src/main/groovy/BuildNativeTask.groovy +++ b/buildSrc/src/main/groovy/BuildNativeTask.groovy @@ -195,13 +195,17 @@ class BuildNativeTask extends DefaultTask{ } def ndkDir = findAndroidNdkDir() - def hostTag = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("mac") - ? "darwin-x86_64" - : "linux-x86_64" + def osName = System.getProperty("os.name").toLowerCase(Locale.ROOT) + def hostTag = osName.contains("win") + ? "windows-x86_64" + : osName.contains("mac") + ? "darwin-x86_64" + : "linux-x86_64" def apiLevel = project.findProperty("androidApiLevel") ?: "21" + def compilerName = "aarch64-linux-android${apiLevel}-clang${osName.contains("win") ? ".cmd" : ""}" def compiler = new File( ndkDir, - "toolchains/llvm/prebuilt/${hostTag}/bin/aarch64-linux-android${apiLevel}-clang" + "toolchains/llvm/prebuilt/${hostTag}/bin/${compilerName}" ) if (!compiler.exists()) { throw new GradleException("Android compiler not found: ${compiler}. Set ANDROID_NDK_HOME or ANDROID_NDK_ROOT.") @@ -224,7 +228,13 @@ class BuildNativeTask extends DefaultTask{ } } - throw new GradleException("Android NDK not found. Set ANDROID_NDK_HOME, ANDROID_NDK_ROOT, ANDROID_HOME, or ANDROID_SDK_ROOT.") + def downloadAndroidNdkTask = project.rootProject.tasks.named("downloadAndroidNdk").get() + def downloadedNdk = downloadAndroidNdkTask.androidNdkHome.asFile + if (downloadedNdk.exists()) { + return downloadedNdk + } + + throw new GradleException("Android NDK not found. Set ANDROID_NDK_HOME, ANDROID_NDK_ROOT, ANDROID_HOME, or ANDROID_SDK_ROOT, or run the downloadAndroidNdk task.") } private void removeWindowsDebugFiles(){ diff --git a/buildSrc/src/main/groovy/DownloadAndroidNdkTask.groovy b/buildSrc/src/main/groovy/DownloadAndroidNdkTask.groovy new file mode 100644 index 00000000..6ffdcd43 --- /dev/null +++ b/buildSrc/src/main/groovy/DownloadAndroidNdkTask.groovy @@ -0,0 +1,117 @@ +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.Directory +import org.gradle.api.file.RegularFile +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform +import org.gradle.nativeplatform.platform.internal.DefaultOperatingSystem +import org.treesitter.build.Utils + +class DownloadAndroidNdkTask extends DefaultTask { + + @Internal + DefaultOperatingSystem os + + DownloadAndroidNdkTask() { + group = "download" + description = "Download Android NDK required for Android native targets" + os = DefaultNativePlatform.currentOperatingSystem + } + + @Input + String getAndroidNdkVersion() { + return project.rootProject.property("androidNdkVersion") + } + + @Internal + Directory getAndroidNdkDir() { + return project.rootProject.layout.buildDirectory.dir("android-ndk").get() + } + + @OutputDirectory + Directory getAndroidNdkHome() { + return androidNdkDir.dir("android-ndk-${androidNdkVersion}") + } + + @Internal + String getOsName() { + if (os.windows) { + return "windows" + } else if (os.macOsX) { + return "darwin" + } else if (os.linux) { + return "linux" + } else { + throw new GradleException("Unsupported OS: " + os.name) + } + } + + @OutputFile + RegularFile getAndroidNdkArchive() { + return androidNdkDir.file("android-ndk-${androidNdkVersion}-${osName}.zip") + } + + @Internal + String getAndroidNdkUrl() { + return "https://dl.google.com/android/repository/${androidNdkArchive.asFile.name}" + } + + @TaskAction + void downloadAndroidNdk() { + def installedNdk = findInstalledAndroidNdkDir() + if (installedNdk != null) { + logger.lifecycle("Using installed Android NDK: {}", installedNdk) + return + } + + if (androidNdkHome.asFile.exists()) { + logger.lifecycle("Using downloaded Android NDK: {}", androidNdkHome.asFile) + return + } + + logger.lifecycle("Downloading Android NDK from {}", androidNdkUrl) + Utils.downloadFile(androidNdkUrl, androidNdkArchive.asFile) + Utils.unzipArchive(androidNdkArchive.asFile, androidNdkDir.asFile) + androidCompiler.asFile.setExecutable(true, true) + } + + @Internal + RegularFile getAndroidCompiler() { + return androidNdkHome.file("toolchains/llvm/prebuilt/${hostTag}/bin/aarch64-linux-android21-clang${os.windows ? '.cmd' : ''}") + } + + @Internal + String getHostTag() { + if (os.windows) { + return "windows-x86_64" + } else if (os.macOsX) { + return "darwin-x86_64" + } else if (os.linux) { + return "linux-x86_64" + } else { + throw new GradleException("Unsupported OS: " + os.name) + } + } + + File findInstalledAndroidNdkDir() { + def envNdk = System.getenv("ANDROID_NDK_HOME") ?: System.getenv("ANDROID_NDK_ROOT") + if (envNdk) { + return new File(envNdk) + } + + def sdkDir = System.getenv("ANDROID_HOME") ?: System.getenv("ANDROID_SDK_ROOT") + if (sdkDir) { + def ndkRoot = new File(sdkDir, "ndk") + def ndks = ndkRoot.listFiles()?.findAll { it.isDirectory() }?.sort { a, b -> b.name <=> a.name } + if (ndks) { + return ndks.first() + } + } + + return null + } +} diff --git a/gradle.properties b/gradle.properties index 1fb29a0b..1729ff2d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,6 +3,7 @@ miniSigDir=minisign zigDir=zig zigVersion=0.11.0 miniSignVersion=0.11 +androidNdkVersion=r27c zigPubKey=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U treeSitterTargets=x86_64-windows,x86_64-macos,x86_64-linux-gnu,aarch64-linux-gnu,aarch64-macos,aarch64-linux-android org.gradle.parallel=true