import expo.modules.plugin.gradle.ExpoModuleExtension
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.transform.CompileStatic
import org.apache.commons.compress.archivers.tar.TarArchiveEntry
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.ValueSource
import org.gradle.api.provider.ValueSourceParameters
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.tukaani.xz.LZMA2Options
import org.tukaani.xz.XZInputStream
import org.tukaani.xz.XZOutputStream

import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.security.MessageDigest
import java.time.Instant
import java.util.regex.Matcher

buildscript {
  // List of features that are required by linked modules
  def coreFeatures = project.findProperty("coreFeatures") ?: []
  ext.shouldIncludeCompose = coreFeatures.contains("compose")

  repositories {
    mavenCentral()
  }

  dependencies {
    if (shouldIncludeCompose) {
      classpath("org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:${kotlinVersion}")
    }

    classpath "org.tukaani:xz:1.12"
    classpath "org.apache.commons:commons-compress:1.28.0"
  }
}

apply plugin: 'com.android.library'
apply plugin: 'expo-module-gradle-plugin'

if (shouldIncludeCompose) {
  apply plugin: 'org.jetbrains.kotlin.plugin.compose'
}

group = 'host.exp.exponent'
version = '58.0.19'

def isExpoModulesCoreTests = {
  Gradle gradle = getGradle()
  String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()
  if (tskReqStr =~ /:expo-modules-core:connected\w*AndroidTest/) {
    def androidTests = project.file("src/androidTest")
    return androidTests.exists() && androidTests.isDirectory()
  }
  return false
}.call()

def isTests = {
  Gradle gradle = getGradle()
  String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()
  return tskReqStr =~ /connected\w*AndroidTest/
}.call()

def expoModuleExtension = project.extensions.getByType(ExpoModuleExtension)

def reactNativeArchitectures() {
  def value = project.getProperties().get("reactNativeArchitectures")
  return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}

// HERMES
def USE_HERMES = findProperty("hermesEnabled") ?: true

// Currently the needs for hermes/jsc are only for androidTest, so we turn on this flag only when `isExpoModulesCoreTests` is true
USE_HERMES = USE_HERMES && isExpoModulesCoreTests
// END HERMES

def shouldTurnWarningsIntoErrors = findProperty("EXPO_TURN_WARNINGS_INTO_ERRORS") == "true"

def enableWorkletsIntegration = !isTests
def workletsProject = enableWorkletsIntegration ? findProject(":react-native-worklets") : null

if (workletsProject != null) {
  evaluationDependsOn(workletsProject.path)

  afterEvaluate {
    println("Linking react-native-worklets native libs into expo-modules-core build tasks")
    println(workletsProject.tasks.getByName("mergeDebugNativeLibs"))
    println(workletsProject.tasks.getByName("mergeReleaseNativeLibs"))
    tasks.getByName("buildCMakeDebug").dependsOn(workletsProject.tasks.getByName("mergeDebugNativeLibs"))
    tasks.getByName("buildCMakeRelWithDebInfo").dependsOn(workletsProject.tasks.getByName("mergeReleaseNativeLibs"))
  }
}

def reactNativeWorkletsRootDir = workletsProject?.projectDir?.parentFile

def expoModulesV2Version = "0.2.5"

configurations {
  // Artifact-only: the C++ sources, not something to put on any classpath.
  expoModulesV2Cpp
}

def expoModulesV2CppDir = layout.buildDirectory.dir("expo-modules-v2-cpp").get().asFile

def unpackExpoModulesV2Cpp = tasks.register("unpackExpoModulesV2Cpp", Sync) {
  it.from(project.provider { zipTree(configurations.expoModulesV2Cpp.singleFile) })
  it.into(expoModulesV2CppDir)
}

// CMake needs the sources on disk before it configures, not just before it builds.
tasks.matching {
  it.name.startsWith("configureCMake") || it.name.startsWith("buildCMake")
}.configureEach {
  it.dependsOn(unpackExpoModulesV2Cpp)
}

def reactNativeVersionString = expoModuleExtension.reactNativeVersion.toString()

def prebuiltNativeLibsDir = project.file("prebuilt")
def prebuiltNativeLibsArchive = project.file("prebuilt/native-libs.tar.xz")
def prebuiltMetadataFile = project.file("prebuilt/metadata.json")
def packageDir = project.projectDir.parentFile

def decompressedNativeLibsDir = layout.buildDirectory.dir("expo-prebuilt-natives").get().asFile

def prebuiltVariants = ["debug", "release"]

def isPrebuiltCachingRun = gradle.startParameter.taskNames.any { it.contains("cachePrebuiltNativeLibs") }

def isPrebuiltUsable = {
  if (!prebuiltMetadataFile.exists() || !prebuiltNativeLibsArchive.exists()) {
    return false
  }

  def metadata = new JsonSlurper().parseText(prebuiltMetadataFile.text)
  if (metadata.reactNativeVersion != reactNativeVersionString) {
    logger.warn("[expo-modules-core] Ignoring prebuilt libs: built for RN ${metadata.reactNativeVersion}, installed RN ${reactNativeVersionString} - building from source")
    return false
  }

  def sourceHash = providers.of(PrebuiltSourceHashValueSource) {
    it.parameters.packageDir.set(packageDir)
  }.get()
  if (metadata.sourceHash != sourceHash) {
    logger.warn("[expo-modules-core] Ignoring prebuilt libs: the C++ sources changed after they were built - building from source. Run the `cachePrebuiltNativeLibs` task to rebuild them")
    return false
  }
  return true
}

def buildFromSourceRequested = findProperty("expo.core.buildFromSource") == "true"
if (buildFromSourceRequested) {
  logger.lifecycle("[expo-modules-core] `expo.core.buildFromSource` is set - building native libraries from source")
}

def usePrebuiltNativeLibs = !isPrebuiltCachingRun && !buildFromSourceRequested && isPrebuiltUsable()

expoModule {
  canBePublished false
}

android {
  if (rootProject.hasProperty("ndkPath")) {
    ndkPath rootProject.ext.ndkPath
  }
  if (rootProject.hasProperty("ndkVersion")) {
    ndkVersion rootProject.ext.ndkVersion
  }

  namespace "expo.modules"
  defaultConfig {
    consumerProguardFiles 'proguard-rules.pro'
    versionCode 1
    versionName "58.0.19"
    buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
    buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"

    testInstrumentationRunner "expo.modules.TestRunner"

    externalNativeBuild {
      cmake {
        def cppArguments = [
          "-DANDROID_STL=c++_shared",
          "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON",
          "-DREACT_NATIVE_DIR=${expoModuleExtension.reactNativeDir}",
          "-DREACT_NATIVE_TARGET_VERSION=${expoModuleExtension.reactNativeVersion.minor}",
          "-DUSE_HERMES=${USE_HERMES}",
          "-DUNIT_TEST=${isExpoModulesCoreTests}",
          "-DEXPO_MODULES_V2_CPP_DIR=${expoModulesV2CppDir.absolutePath}"
        ]

        if (reactNativeWorkletsRootDir != null) {
          cppArguments += "-DREACT_NATIVE_WORKLETS_DIR=${reactNativeWorkletsRootDir.absolutePath}"
        }

        if (usePrebuiltNativeLibs) {
          cppArguments += "-DEXPO_PREBUILT_NATIVE_LIBS_DIR=${decompressedNativeLibsDir.absolutePath}"
        }

        abiFilters(*reactNativeArchitectures())
        arguments(*cppArguments)
      }
    }
  }

  externalNativeBuild {
    cmake {
      path "CMakeLists.txt"
    }
  }

  buildFeatures {
    buildConfig true
    prefab true
    compose shouldIncludeCompose
  }

  packagingOptions {
    // Gradle will add cmake target dependencies into packaging.
    // Theses files are intermediated linking files to build modules-core and should not be in final package.
    def sharedLibraries = [
      "**/libc++_shared.so",
      "**/libfabricjni.so",
      "**/libfbjni.so",
      "**/libfolly_json.so",
      "**/libfolly_runtime.so",
      "**/libglog.so",
      "**/libhermesvm.so",
      "**/libjscexecutor.so",
      "**/libjsi.so",
      "**/libreactnative.so",
      "**/libreactnativejni.so",
      "**/libreact_debug.so",
      "**/libreact_nativemodule_core.so",
      "**/libreact_utils.so",
      "**/libreact_render_debug.so",
      "**/libreact_render_graphics.so",
      "**/libreact_render_core.so",
      "**/libreact_render_componentregistry.so",
      "**/libreact_render_mapbuffer.so",
      "**/librrc_view.so",
      "**/libruntimeexecutor.so",
      "**/libyoga.so",
    ]

    // Required or mockk will crash
    resources {
      excludes += [
        "META-INF/LICENSE.md",
        "META-INF/LICENSE-notice.md"
      ]
    }

    // In android (instrumental) tests, we want to package all so files to enable our JSI functionality.
    // Otherwise, those files should be excluded, because will be loaded by the application.
    if (isExpoModulesCoreTests) {
      pickFirsts += sharedLibraries
    } else {
      excludes += sharedLibraries
    }
  }

  sourceSets {
    main {
      kotlin {
        if (shouldIncludeCompose) {
          srcDirs += 'src/compose'
        } else {
          srcDirs += 'src/withoutCompose'
        }
      }
    }

    // Package the decompressed prebuilt .so into each variant. The layout `<variant>/<abi>/lib*.so`
    // matches what AGP expects from a jniLibs source set.
    if (usePrebuiltNativeLibs) {
      prebuiltVariants.each { variant ->
        getByName(variant).jniLibs.srcDir(new File(decompressedNativeLibsDir, variant))
      }
    }
  }

  testOptions {
    unitTests.includeAndroidResources = true

    unitTests.all { test ->
      testLogging {
        outputs.upToDateWhen { false }
        events "passed", "failed", "skipped", "standardError"
        showCauses true
        showExceptions true
        showStandardStreams true
      }
    }
  }
}

dependencies {
  implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${kotlinVersion}"
  implementation "org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}"
  implementation 'androidx.annotation:annotation:1.9.1'

  api "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2"
  api "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
  api "androidx.core:core-ktx:1.17.0"

  if (shouldIncludeCompose) {
    implementation 'androidx.compose.foundation:foundation-android:1.7.6'
  }

  implementation("androidx.tracing:tracing-ktx:1.2.0")

  implementation 'com.facebook.react:react-android'

  compileOnly 'com.facebook.fbjni:fbjni:0.5.1'

  if (workletsProject != null) {
    implementation workletsProject
  }

  implementation("io.github.expo.pika:pika-api:1.1.0")

  implementation("io.github.expo.kolibri:kolibri-android")
  api("io.github.expo:expo-modules-v2-react:${expoModulesV2Version}")
  expoModulesV2Cpp("io.github.expo:expo-modules-v2-react:${expoModulesV2Version}:cpp@zip")

  testImplementation 'androidx.test:core:1.7.0'
  testImplementation 'junit:junit:4.13.2'
  testImplementation 'io.mockk:mockk:1.14.9'
  testImplementation "com.google.truth:truth:1.4.5"
  testImplementation "org.robolectric:robolectric:4.16.1"
  testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2"
  testImplementation "org.json:json:20250517"

  androidTestImplementation 'androidx.test:runner:1.7.0'
  androidTestImplementation 'androidx.test:core:1.7.0'
  androidTestImplementation 'androidx.test:rules:1.7.0'
  androidTestImplementation "io.mockk:mockk-android:1.14.9"
  androidTestImplementation "com.google.truth:truth:1.4.5"
  androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2"

  if (isExpoModulesCoreTests) {
    if (USE_HERMES) {
      compileOnly "com.facebook.react:hermes-android"
    } else {
      compileOnly "org.webkit:android-jsc:+"
    }
  }
}

if (shouldTurnWarningsIntoErrors) {
  tasks.withType(JavaCompile) configureEach {
    options.compilerArgs << "-Werror" << "-Xlint:all" << '-Xlint:-serial' << '-Xlint:-rawtypes'
  }
  tasks.withType(KotlinCompile) configureEach {
    compilerOptions.allWarningsAsErrors = true
  }
}

// Generates minimal stub PCH files from an empty header so the IDE's C++ engine
// doesn't fail during sync. Near-instant unlike the full ExpoHeader.pch.
// The actual build regenerates proper PCH files via ninja.
def cxxDir = project.file(".cxx")
def generateStubPCHTask = tasks.register("generateStubPCH") {
  dependsOn("configureCMakeDebug")

  doLast {
    if (!cxxDir.exists()) {
      return
    }

    cxxDir.eachFileRecurse { file ->
      if (file.name != "compile_commands.json") {
        return
      }

      new JsonSlurper().parseText(file.text).each { entry ->
        if (!entry.file.endsWith("cmake_pch.hxx.cxx")) {
          return
        }

        def pchFile = new File(entry.file.substring(0, entry.file.length() - ".cxx".length()) + ".pch")

        if (!pchFile.exists() || pchFile.length() == 0) {
          def stubHeader = new File(entry.directory, "stub_pch.hxx")
          stubHeader.text = ""

          def stubHeaderPath = stubHeader.absolutePath.replace('\\', '/')

          def cmd = entry.command
          // Replace the forced-include path: `-Xclang -include -Xclang <path>/cmake_pch.hxx`
            .replaceAll(/-Xclang -include -Xclang [^\s]+cmake_pch\.hxx(?=\s)/, Matcher.quoteReplacement("-Xclang -include -Xclang ${stubHeaderPath}"))
          // Replace the source file operand: `<path>/cmake_pch.hxx.cxx`
            .replaceAll(/[^\s]+cmake_pch\.hxx\.cxx/, Matcher.quoteReplacement(stubHeaderPath))

          def process = new ProcessBuilder(cmd.split(" ").toList())
            .directory(new File(entry.directory))
            .redirectErrorStream(true)
            .start()
          if (process.waitFor() != 0) {
            throw new GradleException("Stub PCH generation failed: ${process.inputStream.text}")
          }
        }

        // Ensure PCH is older than source so ninja rebuilds the real one during build
        pchFile.setLastModified(new File(entry.file).lastModified() - 1)
      }
    }
  }
}

tasks.register("prepareKotlinBuildScriptModel") {
  dependsOn(generateStubPCHTask)
}

def prebuiltNativeLibNames = ["libexpo-modules-core.so", "libexpo-modules-jsi.so"]

class PrebuiltNativeLibs {
  /**
   * Package-relative files and directories compiled into the prebuilt libs.
   */
  static final List<String> SOURCE_ROOTS = [
    "android/CMakeLists.txt",
    "android/cmake",
    "android/src/jsi",
    "android/src/main/cpp",
    "common/cpp",
  ]

  /**
   * Paths inside `SOURCE_ROOTS` that build separate libraries, which are never prebuilt.
   */
  static final List<String> SOURCE_EXCLUDES = [
    "android/cmake/tests.cmake",
    "android/cmake/worklets.cmake",
    "android/src/main/cpp/tests",
    "android/src/main/cpp/worklets",
  ]

  /**
   * MD5 over every source file, ordered by package-relative path.
   * Statically compiled, because it runs in every build and dynamic Groovy dispatch doubles its cost.
   */
  @CompileStatic
  static String computeSourceHash(File packageDir) {
    List<String> relativePaths = new ArrayList<String>()
    for (String root : SOURCE_ROOTS) {
      collectSourceFiles(packageDir, root, relativePaths)
    }
    Collections.sort(relativePaths)

    MessageDigest digest = MessageDigest.getInstance("MD5")
    byte[] buffer = new byte[8192]
    for (String relativePath : relativePaths) {
      digest.update(relativePath.getBytes("UTF-8"))
      digest.update((byte) 0)
      InputStream input = Files.newInputStream(new File(packageDir, relativePath).toPath())
      try {
        int read
        while ((read = input.read(buffer)) != -1) {
          digest.update(buffer, 0, read)
        }
      } finally {
        input.close()
      }
      digest.update((byte) 0)
    }
    return digest.digest().encodeHex().toString()
  }

  @CompileStatic
  private static void collectSourceFiles(File packageDir, String relativePath, List<String> result) {
    if (SOURCE_EXCLUDES.contains(relativePath)) {
      return
    }
    File file = new File(packageDir, relativePath)
    if (file.isDirectory()) {
      String[] names = file.list()
      if (names == null) {
        return
      }
      for (String name : names) {
        if (!name.startsWith(".")) {
          collectSourceFiles(packageDir, relativePath + "/" + name, result)
        }
      }
    } else if (file.isFile()) {
      result.add(relativePath)
    }
  }

  /**
   * Returns host tag based on the OS name.
   */
  private static String hostTag() {
    def osName = System.getProperty("os.name").toLowerCase()
    if (osName.contains("mac")) {
      return "darwin-x86_64"
    }

    if (osName.contains("win")) {
      return "windows-x86_64"
    }

    return "linux-x86_64"
  }

  private static void strippedLib(File stripBinary, File source, File dest) {
    def proc = new ProcessBuilder(
      stripBinary.absolutePath,
      "--strip-all",
      source.absolutePath,
      "-o",
      dest.absolutePath
    )
      .redirectErrorStream(true)
      .start()

    if (proc.waitFor() != 0) {
      throw new GradleException("[expo-modules-core] llvm-strip failed for ${source}: ${proc.inputStream.text}")
    }
  }

  /**
   * Locate the NDK's `llvm-strip`, or null when the NDK/binary can't be found.
   */
  static File findLlvmStrip(File ndkDir) {
    if (ndkDir == null || !ndkDir.exists()) {
      return null
    }
    def hostTag = hostTag()
    def strip = new File(ndkDir, "toolchains/llvm/prebuilt/${hostTag}/bin/llvm-strip")
    return strip.exists() ? strip : null
  }

  /**
   * Copy each of `soNames` from `objDir/<abi>/` into `stagingVariantDir/<abi>/`, stripping debug
   * symbols. Returns the set of ABIs that were staged.
   */
  static Set<String> stageStrippedLibs(File objDir, List<String> soNames, File stagingVariantDir, File stripBinary) {
    def abis = new TreeSet<String>()
    objDir
      .listFiles({ it.isDirectory() } as FileFilter)
      ?.each { abiDir ->
        soNames.each { soName ->
          def source = new File(abiDir, soName)
          if (!source.exists()) {
            return
          }

          def dest = new File(stagingVariantDir, "${abiDir.name}/${soName}")
          dest.parentFile.mkdirs()

          if (stripBinary != null) {
            strippedLib(stripBinary, source, dest)
          } else {
            Files.copy(source.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING)
          }

          abis << abiDir.name
        }
      }
    return abis
  }

  /**
   * Bundle every file under `stagingDir` into a single solid tar.xz
   */
  static void bundleToTarXz(File stagingDir, File archive) {
    archive.parentFile.mkdirs()
    def stagingPath = stagingDir.toPath()

    def files = []
    stagingDir.eachFileRecurse {
      if (it.isFile()) {
        files << it
      }
    }
    files.sort { it.absolutePath }

    new TarArchiveOutputStream(
      new XZOutputStream(archive.newOutputStream(), new LZMA2Options(9))
    ).withCloseable { tar ->
      files.each { file ->
        def archiveEntry = new TarArchiveEntry(file, stagingPath.relativize(file.toPath()).toString())
        tar.putArchiveEntry(archiveEntry)
        Files.copy(file.toPath(), tar)
        tar.closeArchiveEntry()
      }
    }
  }
}

interface PrebuiltSourceHashParameters extends ValueSourceParameters {
  DirectoryProperty getPackageDir()
}

abstract class PrebuiltSourceHashValueSource implements ValueSource<String, PrebuiltSourceHashParameters> {
  @Override
  String obtain() {
    return PrebuiltNativeLibs.computeSourceHash(parameters.packageDir.get().asFile)
  }
}

def stripBinary = PrebuiltNativeLibs.findLlvmStrip(android.ndkDirectory)
def cacheStagingDir = layout.buildDirectory.dir("tmp/cache-prebuilt-natives").get().asFile
def variantDescriptors = prebuiltVariants.collect { variant ->
  def variantCap = variant.capitalize()
  def libsDir = layout.buildDirectory.dir("intermediates/merged_native_libs/${variant}/merge${variantCap}NativeLibs/out/lib").get().asFile
  def outDir = layout.buildDirectory.dir("tmp/cache-prebuilt-natives/${variant}").get().asFile
  [
    name     : variant,
    mergeTask: "merge${variantCap}NativeLibs",
    libsDir  : libsDir,
    outDir   : outDir,
  ]
}

tasks.register("cachePrebuiltNativeLibs") {
  group = "prebuilt natives"
  description = "Build expo-modules-core from source (debug + release), strip and cache the shippable .so files into prebuilt/"
  dependsOn(variantDescriptors.collect { it.mergeTask })

  doLast {
    if (stripBinary == null) {
      logger.warn("[expo-modules-core] NDK llvm-strip not found - .so files will be cached unstripped")
    }

    if (prebuiltNativeLibsDir.exists()) {
      prebuiltNativeLibsDir.deleteDir()
    }

    if (cacheStagingDir.exists()) {
      cacheStagingDir.deleteDir()
    }

    def allAbis = new TreeSet<String>()
    variantDescriptors.each { vd ->
      logger.lifecycle("[expo-modules-core] Caching ${vd.name} from ${vd.libsDir}")
      allAbis.addAll(PrebuiltNativeLibs.stageStrippedLibs(vd.libsDir, prebuiltNativeLibNames, vd.outDir, stripBinary))
    }
    if (allAbis.isEmpty()) {
      throw new GradleException("[expo-modules-core] No ${prebuiltNativeLibNames} were cached")
    }

    PrebuiltNativeLibs.bundleToTarXz(cacheStagingDir, prebuiltNativeLibsArchive)
    cacheStagingDir.deleteDir()

    prebuiltMetadataFile.text = JsonOutput.prettyPrint(JsonOutput.toJson([
      reactNativeVersion: reactNativeVersionString,
      sourceHash        : PrebuiltNativeLibs.computeSourceHash(packageDir),
      variants          : variantDescriptors.collect { it.name },
      abis              : allAbis as List,
      soFiles           : prebuiltNativeLibNames as List,
      stripped          : stripBinary != null,
      createdAt          : Instant.now().toString(),
    ])) + "\n"

    def archiveSizeMb = prebuiltNativeLibsArchive.length() / (1024 * 1024)
    logger.lifecycle(
      "[expo-modules-core] Create ${prebuiltNativeLibNames.join(", ")} for ${allAbis.size()} ABIs " +
        "(${allAbis.join(", ")}) x ${variantDescriptors.size()} variants into " +
        "${prebuiltNativeLibsArchive.name} (~${archiveSizeMb}MB, stripped, solid tar.xz)"
    )
  }
}

// Unpacks the shipped solid `prebuilt/native-libs.tar.xz` into the build dir so CMake can import the
// libs and AGP can package them via the jniLibs source sets
if (usePrebuiltNativeLibs) {
  def archiveFile = prebuiltNativeLibsArchive
  def outputDir = decompressedNativeLibsDir
  def decompressPrebuiltNativeLibs = tasks.register("decompressPrebuiltNativeLibs") {
    description = "Unpack the shipped native-libs.tar.xz into the build dir for CMake import and packaging"
    inputs.file(archiveFile)
    outputs.dir(outputDir)

    doLast {
      outputDir.deleteDir()
      new TarArchiveInputStream(
        new XZInputStream(archiveFile.newInputStream())
      ).withCloseable { tar ->
        def entry
        while ((entry = tar.nextEntry) != null) {
          if (entry.isDirectory()) {
            continue
          }
          // entries are `<variant>/<abi>/lib*.so`
          def dest = new File(outputDir, entry.name)
          dest.parentFile.mkdirs()
          dest.withOutputStream { out -> tar.transferTo(out) }
        }
      }
    }
  }

  tasks.named("preBuild").configure { dependsOn(decompressPrebuiltNativeLibs) }
}
