Snap for 8191477 from 446b435418361a872663352949eb46afae1ab506 to tm-frc-resolv-release

Change-Id: I9f99795a8293e99640fec1ed37879db95cb584cb
diff --git a/.gitignore b/.gitignore
index 14e1cbc..8f8ed69 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,10 @@
 .gradle
 /build/
+/buildSrc/build/
+/curl/build/
+/googletest/build/
+/jsoncpp/build/
+/openssl/build/
 
 # Ignore Gradle GUI config
 gradle-app.setting
@@ -43,11 +48,11 @@
 # When using Gradle or Maven with auto-import, you should exclude module files,
 # since they will be recreated, and may cause churn.  Uncomment if using
 # auto-import.
-# .idea/modules.xml
-# .idea/*.iml
-# .idea/modules
-# *.iml
-# *.ipr
+.idea/modules.xml
+.idea/*.iml
+.idea/modules
+*.iml
+*.ipr
 
 # CMake
 cmake-build-*/
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index e3439a9..93ae7ac 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -10,6 +10,7 @@
     </JetCodeStyleSettings>
     <codeStyleSettings language="kotlin">
       <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+      <option name="KEEP_LINE_BREAKS" value="false" />
       <option name="WRAP_ON_TYPING" value="0" />
     </codeStyleSettings>
   </code_scheme>
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..443b5d2
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="CompilerConfiguration">
+    <bytecodeTargetLevel target="1.6" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 0000000..6fde95a
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="RemoteRepositoriesConfiguration">
+    <remote-repository>
+      <option name="id" value="central" />
+      <option name="name" value="Maven Central repository" />
+      <option name="url" value="https://repo1.maven.org/maven2" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="jboss.community" />
+      <option name="name" value="JBoss Community repository" />
+      <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="MavenRepo" />
+      <option name="name" value="MavenRepo" />
+      <option name="url" value="https://repo.maven.apache.org/maven2/" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="maven" />
+      <option name="name" value="maven" />
+      <option name="url" value="https://dl.bintray.com/s1m0nw1/KtsRunner" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="BintrayJCenter" />
+      <option name="name" value="BintrayJCenter" />
+      <option name="url" value="https://jcenter.bintray.com/" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="Google" />
+      <option name="name" value="Google" />
+      <option name="url" value="https://dl.google.com/dl/android/maven2/" />
+    </remote-repository>
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/kotlinScripting.xml b/.idea/kotlinScripting.xml
index a6fe551..e56386c 100644
--- a/.idea/kotlinScripting.xml
+++ b/.idea/kotlinScripting.xml
@@ -1,6 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
   <component name="KotlinScriptingSettings">
-    <option name="isAutoReloadEnabled" value="true" />
+    <scriptDefinition className="org.jetbrains.kotlin.scripting.resolve.KotlinScriptDefinitionFromAnnotatedTemplate" definitionName="KotlinBuildScript">
+      <order>2147483647</order>
+      <autoReloadConfigurations>true</autoReloadConfigurations>
+    </scriptDefinition>
   </component>
 </project>
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index bc8d0a3..15b559a 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
   <component name="ExternalStorageConfigurationManager" enabled="true" />
-  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_6" default="false" project-jdk-name="1.8" project-jdk-type="JavaSDK">
     <output url="file://$PROJECT_DIR$/out" />
   </component>
 </project>
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 347b2d7..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="ProjectModuleManager">
-    <modules>
-      <module fileurl="file://$PROJECT_DIR$/.idea/ndkports.iml" filepath="$PROJECT_DIR$/.idea/ndkports.iml" />
-    </modules>
-  </component>
-</project>
\ No newline at end of file
diff --git a/.idea/ndkports.iml b/.idea/ndkports.iml
deleted file mode 100644
index d6ebd48..0000000
--- a/.idea/ndkports.iml
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<module type="JAVA_MODULE" version="4">
-  <component name="NewModuleRootManager" inherit-compiler-output="true">
-    <exclude-output />
-    <content url="file://$MODULE_DIR$" />
-    <orderEntry type="inheritedJdk" />
-    <orderEntry type="sourceFolder" forTests="false" />
-  </component>
-</module>
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 73193ea..899b2f1 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -27,6 +27,34 @@
           <option name="issueRegexp" value="\bcs/([^\s]+[\w$])" />
           <option name="linkRegexp" value="https://cs.corp.google.com/search/?q=$1" />
         </IssueNavigationLink>
+        <IssueNavigationLink>
+          <option name="issueRegexp" value="\bb/(\d+)(#\w+)?\b" />
+          <option name="linkRegexp" value="https://buganizer.corp.google.com/issues/$1$2" />
+        </IssueNavigationLink>
+        <IssueNavigationLink>
+          <option name="issueRegexp" value="\b(?:BUG=|FIXED=)(\d+)\b" />
+          <option name="linkRegexp" value="https://buganizer.corp.google.com/issues/$1" />
+        </IssueNavigationLink>
+        <IssueNavigationLink>
+          <option name="issueRegexp" value="\b(?:cl/|cr/|OCL=|DIFFBASE=|ROLLBACK_OF=)(\d+)\b" />
+          <option name="linkRegexp" value="https://critique.corp.google.com/$1" />
+        </IssueNavigationLink>
+        <IssueNavigationLink>
+          <option name="issueRegexp" value="\bomg/(\d+)\b" />
+          <option name="linkRegexp" value="https://omg.corp.google.com/$1" />
+        </IssueNavigationLink>
+        <IssueNavigationLink>
+          <option name="issueRegexp" value="\b(?:go/|goto/)([^,.&lt;&gt;()&quot;\s]+(?:[.,][^,.&lt;&gt;()&quot;\s]+)*)" />
+          <option name="linkRegexp" value="https://goto.google.com/$1" />
+        </IssueNavigationLink>
+        <IssueNavigationLink>
+          <option name="issueRegexp" value="\bcs/([^\s]+[\w$])" />
+          <option name="linkRegexp" value="https://cs.corp.google.com/search/?q=$1" />
+        </IssueNavigationLink>
+        <IssueNavigationLink>
+          <option name="issueRegexp" value="(LINT\.IfChange)|(LINT\.ThenChange)" />
+          <option name="linkRegexp" value="https://goto.google.com/ifthisthenthatlint" />
+        </IssueNavigationLink>
       </list>
     </option>
   </component>
diff --git a/Dockerfile b/Dockerfile
index aba9898..ac011ef 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,15 +1,20 @@
-FROM gcr.io/cloud-builders/gradle:5.6.2-jdk-8
+FROM gcr.io/cloud-builders/javac:8
 
-RUN apt-get update
-RUN apt-get install -y curl
-RUN apt-get install -y ninja-build
-RUN apt-get install -y python3-pip
+RUN apt-get update && apt-get install -y \
+    cmake \
+    curl \
+    ninja-build \
+    python3-pip
 RUN pip3 install meson
 RUN curl -o ndk.zip \
-    https://dl.google.com/android/repository/android-ndk-r20b-linux-x86_64.zip
+    https://dl.google.com/android/repository/android-ndk-r21e-linux-x86_64.zip
 RUN unzip ndk.zip
-RUN mv android-ndk-r20b /ndk
+RUN mv android-ndk-r21e /ndk
+RUN curl -L -o platform-tools.zip \
+    https://dl.google.com/android/repository/platform-tools-latest-linux.zip
+RUN unzip platform-tools.zip platform-tools/adb
+RUN mv platform-tools/adb /usr/bin/adb
 
 WORKDIR /src
 ENTRYPOINT ["./gradlew"]
-CMD ["-PndkPath=/ndk", "release"]
+CMD ["--stacktrace", "-PndkPath=/ndk", "release"]
diff --git a/build.gradle.kts b/build.gradle.kts
index a00d626..f217598 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,113 +1,47 @@
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+buildscript {
+    val snapshotSuffix = if (hasProperty("release")) {
+        // We're still tagging releases as betas until we have more thorough
+        // test automation.
+        "-beta-1"
+    } else {
+        "-SNAPSHOT"
+    }
 
-plugins {
-    kotlin("jvm") version "1.3.72"
-    application
+    extra.apply {
+        set("snapshotSuffix", snapshotSuffix)
+    }
 }
 
 group = "com.android"
-version = "1.0.0-SNAPSHOT"
+version = "1.0.0${extra.get("snapshotSuffix")}"
+
+plugins {
+    distribution
+}
 
 repositories {
     mavenCentral()
     jcenter()
     google()
-    maven(url = "https://dl.bintray.com/s1m0nw1/KtsRunner")
-}
-
-dependencies {
-    implementation(kotlin("stdlib", "1.3.72"))
-    implementation(kotlin("reflect", "1.3.72"))
-
-    implementation("com.google.prefab:api:1.0.0")
-
-    implementation("com.github.ajalt:clikt:2.2.0")
-    implementation("de.swirtz:ktsRunner:0.0.7")
-    implementation("org.apache.maven:maven-core:3.6.2")
-    implementation("org.redundent:kotlin-xml-builder:1.5.3")
-
-    testImplementation("org.jetbrains.kotlin:kotlin-test")
-    testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
-    testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0-M1")
-    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.0-M1")
-}
-
-application {
-    // Define the main class for the application.
-    mainClassName = "com.android.ndkports.CliKt"
-}
-
-tasks.withType<KotlinCompile> {
-    kotlinOptions.jvmTarget = "1.8"
-    kotlinOptions.freeCompilerArgs += listOf(
-        "-progressive",
-        "-Xuse-experimental=kotlinx.serialization.ImplicitReflectionSerializer"
-    )
-}
-
-val portsBuildDir = buildDir.resolve("ports")
-
-val allPorts = listOf("openssl", "curl", "jsoncpp")
-
-// Can be specified in ~/.gradle/gradle.properties:
-//
-//     ndkPath=/path/to/ndk
-//
-// Or on the command line:
-//
-//     ./gradlew -PndkPath=/path/to/ndk run
-val ndkPath: String by project
-tasks.named<JavaExec>("run") {
-    // Order matters since we don't do any dependency sorting, so we can't just
-    // use the directory list.
-    args = listOf("--ndk", ndkPath, "-o", portsBuildDir.toString()) + allPorts
-}
-
-for (port in allPorts) {
-    distributions {
-        create(port) {
-            contents {
-                includeEmptyDirs = false
-                from(portsBuildDir.resolve(port)) {
-                    include("**/*.aar")
-                    include("**/*.pom")
-                }
-            }
-        }
-    }
-
-    tasks.named("${port}DistTar") {
-        dependsOn(":run")
-    }
-
-    tasks.named("${port}DistZip") {
-        dependsOn(":run")
-    }
 }
 
 distributions {
-    create("all") {
+    main {
         contents {
-            includeEmptyDirs = false
-            from(portsBuildDir) {
-                include("**/*.aar")
-                include("**/*.pom")
-            }
+            from("${rootProject.buildDir}/repository")
+            include("**/*.aar")
+            include("**/*.pom")
         }
     }
 }
 
-tasks.named("allDistTar") {
-    dependsOn(":run")
-}
-
-tasks.named("allDistZip") {
-    dependsOn(":run")
+tasks {
+    distZip {
+        dependsOn(project.getTasksByName("publish", true))
+    }
 }
 
 tasks.register("release") {
-    dependsOn(":allDistZip")
-    for (port in allPorts) {
-        dependsOn(":${port}DistZip")
-    }
+    dependsOn(project.getTasksByName("test", true))
+    dependsOn(":distZip")
 }
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
new file mode 100644
index 0000000..4de21c7
--- /dev/null
+++ b/buildSrc/build.gradle.kts
@@ -0,0 +1,62 @@
+val kotlinVersion = "1.4.20"
+
+plugins {
+    id("org.jetbrains.kotlin.jvm") version "1.4.20"
+    id("java-gradle-plugin")
+    id("maven-publish")
+}
+
+group = "com.android.ndkports"
+version = "1.0.0-SNAPSHOT"
+
+repositories {
+    mavenCentral()
+    jcenter()
+    google()
+}
+
+dependencies {
+    implementation(kotlin("stdlib", kotlinVersion))
+    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1")
+
+    implementation("com.google.prefab:api:1.1.2")
+    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.0")
+    implementation("org.redundent:kotlin-xml-builder:1.6.1")
+
+    testImplementation(kotlin("test", kotlinVersion))
+    testImplementation(kotlin("test-junit", kotlinVersion))
+    testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.0")
+    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.0")
+}
+
+tasks {
+    compileJava {
+        @Suppress("UnstableApiUsage")
+        options.release.set(8)
+    }
+
+    compileKotlin {
+        kotlinOptions.jvmTarget = "1.8"
+    }
+
+    compileTestKotlin {
+        kotlinOptions.jvmTarget = "1.8"
+    }
+}
+
+gradlePlugin {
+    plugins {
+        create("ndkports") {
+            id = "com.android.ndkports.NdkPorts"
+            implementationClass = "com.android.ndkports.NdkPortsPlugin"
+        }
+    }
+}
+
+publishing {
+    repositories {
+        maven {
+            url = uri("${rootProject.buildDir}/repository")
+        }
+    }
+}
diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/buildSrc/settings.gradle.kts
diff --git a/buildSrc/src/main/kotlin/com/android/ndkports/Abi.kt b/buildSrc/src/main/kotlin/com/android/ndkports/Abi.kt
new file mode 100644
index 0000000..155c59c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/com/android/ndkports/Abi.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ndkports
+
+import java.util.Collections.max
+
+enum class Abi(
+    val archName: String,
+    val abiName: String,
+    val triple: String,
+    val minSupportedVersion: Int
+) {
+    Arm("arm", "armeabi-v7a", "arm-linux-androideabi", 16),
+    Arm64("arm64", "arm64-v8a", "aarch64-linux-android", 21),
+    X86("x86", "x86", "i686-linux-android", 16),
+    X86_64("x86_64", "x86_64", "x86_64-linux-android", 21);
+
+    fun adjustMinSdkVersion(minSdkVersion: Int) =
+        max(listOf(minSdkVersion, minSupportedVersion))
+
+    companion object {
+        fun fromAbiName(name: String) = values().find { it.abiName == name }
+    }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/com/android/ndkports/AdHocPortTask.kt b/buildSrc/src/main/kotlin/com/android/ndkports/AdHocPortTask.kt
new file mode 100644
index 0000000..4c9fcbc
--- /dev/null
+++ b/buildSrc/src/main/kotlin/com/android/ndkports/AdHocPortTask.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ndkports
+
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import java.io.File
+
+open class RunBuilder {
+    val cmd = mutableListOf<String>()
+    fun arg(arg: String) = cmd.add(arg)
+    fun args(vararg args: String) = cmd.addAll(args)
+
+    val env = mutableMapOf<String, String>()
+    fun env(key: String, value: String) = env.set(key, value)
+}
+
+class AdHocBuilder(
+    val sourceDirectory: File,
+    val buildDirectory: File,
+    val installDirectory: File,
+    val toolchain: Toolchain,
+    val sysroot: File,
+    val ncpus: Int,
+) {
+    val runs = mutableListOf<RunBuilder>()
+    fun run(block: RunBuilder.() -> Unit) {
+        runs.add(RunBuilder().apply { block() })
+    }
+}
+
+abstract class AdHocPortTask : PortTask() {
+    @get:Input
+    abstract val builder: Property<AdHocBuilder.() -> Unit>
+
+    fun builder(block: AdHocBuilder.() -> Unit) = builder.set(block)
+
+    override fun buildForAbi(
+        toolchain: Toolchain,
+        workingDirectory: File,
+        buildDirectory: File,
+        installDirectory: File
+    ) {
+        buildDirectory.mkdirs()
+
+        val builderBlock = builder.get()
+        val builder = AdHocBuilder(
+            sourceDirectory.get().asFile,
+            buildDirectory,
+            installDirectory,
+            toolchain,
+            prefabGenerated.get().asFile,
+            ncpus,
+        )
+        builder.builderBlock()
+
+        for (run in builder.runs) {
+            executeSubprocess(
+                run.cmd, buildDirectory, additionalEnvironment = run.env
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/com/android/ndkports/AutoconfPortTask.kt b/buildSrc/src/main/kotlin/com/android/ndkports/AutoconfPortTask.kt
new file mode 100644
index 0000000..e288e33
--- /dev/null
+++ b/buildSrc/src/main/kotlin/com/android/ndkports/AutoconfPortTask.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ndkports
+
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import java.io.File
+
+class AutoconfBuilder(val toolchain: Toolchain, val sysroot: File) :
+    RunBuilder()
+
+abstract class AutoconfPortTask : PortTask() {
+    @get:Input
+    abstract val autoconf: Property<AutoconfBuilder.() -> Unit>
+
+    fun autoconf(block: AutoconfBuilder.() -> Unit) = autoconf.set(block)
+
+    override fun buildForAbi(
+        toolchain: Toolchain,
+        workingDirectory: File,
+        buildDirectory: File,
+        installDirectory: File
+    ) {
+        buildDirectory.mkdirs()
+
+        val autoconfBlock = autoconf.get()
+        val builder = AutoconfBuilder(
+            toolchain,
+            prefabGenerated.get().asFile.resolve(toolchain.abi.triple)
+        )
+        builder.autoconfBlock()
+
+        executeSubprocess(listOf(
+            "${sourceDirectory.get().asFile.absolutePath}/configure",
+            "--host=${toolchain.binutilsTriple}",
+            "--prefix=${installDirectory.absolutePath}"
+        ) + builder.cmd,
+            buildDirectory,
+            additionalEnvironment = mutableMapOf(
+                "AR" to toolchain.ar.absolutePath,
+                "CC" to toolchain.clang.absolutePath,
+                "CXX" to toolchain.clangxx.absolutePath,
+                "RANLIB" to toolchain.ranlib.absolutePath,
+                "STRIP" to toolchain.strip.absolutePath,
+                "PATH" to "${toolchain.binDir}:${System.getenv("PATH")}"
+            ).apply { putAll(builder.env) })
+
+        executeSubprocess(listOf("make", "-j$ncpus"), buildDirectory)
+
+        executeSubprocess(
+            listOf("make", "-j$ncpus", "install"), buildDirectory
+        )
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/android/ndkports/CMakeCompatibleVersion.kt b/buildSrc/src/main/kotlin/com/android/ndkports/CMakeCompatibleVersion.kt
similarity index 97%
rename from src/main/kotlin/com/android/ndkports/CMakeCompatibleVersion.kt
rename to buildSrc/src/main/kotlin/com/android/ndkports/CMakeCompatibleVersion.kt
index 6d3ee45..ba58ccb 100644
--- a/src/main/kotlin/com/android/ndkports/CMakeCompatibleVersion.kt
+++ b/buildSrc/src/main/kotlin/com/android/ndkports/CMakeCompatibleVersion.kt
@@ -16,7 +16,7 @@
 
 package com.android.ndkports
 
-import java.io.File
+import java.io.Serializable
 
 /**
  * A version number that is compatible with CMake's package version format.
@@ -31,7 +31,7 @@
     val minor: Int?,
     val patch: Int?,
     val tweak: Int?
-) {
+) : Serializable {
     init {
         if (tweak != null) {
             require(patch != null)
diff --git a/buildSrc/src/main/kotlin/com/android/ndkports/CMakePortTask.kt b/buildSrc/src/main/kotlin/com/android/ndkports/CMakePortTask.kt
new file mode 100644
index 0000000..dba2017
--- /dev/null
+++ b/buildSrc/src/main/kotlin/com/android/ndkports/CMakePortTask.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ndkports
+
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import java.io.File
+
+class CMakeBuilder(val toolchain: Toolchain, val sysroot: File) :
+    RunBuilder()
+
+abstract class CMakePortTask : PortTask() {
+    @get:Input
+    abstract val cmake: Property<CMakeBuilder.() -> Unit>
+
+    fun cmake(block: CMakeBuilder.() -> Unit) = cmake.set(block)
+
+    override fun buildForAbi(
+        toolchain: Toolchain,
+        workingDirectory: File,
+        buildDirectory: File,
+        installDirectory: File
+    ) {
+        configure(toolchain, buildDirectory, installDirectory)
+        build(buildDirectory)
+        install(buildDirectory)
+    }
+
+    private fun configure(
+        toolchain: Toolchain, buildDirectory: File, installDirectory: File
+    ) {
+        val cmakeBlock = cmake.get()
+        val builder = CMakeBuilder(
+            toolchain,
+            prefabGenerated.get().asFile.resolve(toolchain.abi.triple)
+        )
+        builder.cmakeBlock()
+
+        val toolchainFile =
+            toolchain.ndk.path.resolve("build/cmake/android.toolchain.cmake")
+
+        buildDirectory.mkdirs()
+        executeSubprocess(
+            listOf(
+                "cmake",
+                "-DCMAKE_TOOLCHAIN_FILE=${toolchainFile.absolutePath}",
+                "-DCMAKE_BUILD_TYPE=RelWithDebInfo",
+                "-DCMAKE_INSTALL_PREFIX=${installDirectory.absolutePath}",
+                "-DANDROID_ABI=${toolchain.abi.abiName}",
+                "-DANDROID_API_LEVEL=${toolchain.api}",
+                "-GNinja",
+                sourceDirectory.get().asFile.absolutePath,
+            ) + builder.cmd, buildDirectory, builder.env
+        )
+    }
+
+    private fun build(buildDirectory: File) =
+        executeSubprocess(listOf("ninja", "-v"), buildDirectory)
+
+    private fun install(buildDirectory: File) =
+        executeSubprocess(listOf("ninja", "-v", "install"), buildDirectory)
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/com/android/ndkports/Devices.kt b/buildSrc/src/main/kotlin/com/android/ndkports/Devices.kt
new file mode 100644
index 0000000..3ed1d9a
--- /dev/null
+++ b/buildSrc/src/main/kotlin/com/android/ndkports/Devices.kt
@@ -0,0 +1,89 @@
+package com.android.ndkports
+
+import java.io.File
+
+data class AdbException(val args: Iterable<String>, val output: String) :
+    RuntimeException("${formatCmd(args)}:\n$output") {
+    val cmd: String by lazy { formatCmd(args) }
+
+    companion object {
+        fun formatCmd(args: Iterable<String>) = args.joinToString(" ")
+    }
+}
+
+private fun adb(args: Iterable<String>, serial: String? = null): String {
+    val adbCmd = if (serial == null) {
+        listOf("adb")
+    } else {
+        listOf("adb", "-s", serial)
+    }
+    val result = ProcessBuilder(adbCmd + args).redirectErrorStream(true).start()
+    val output = result.inputStream.bufferedReader().use { it.readText() }
+    if (result.waitFor() != 0) {
+        throw AdbException(args, output)
+    }
+    return output
+}
+
+data class Device(val serial: String) {
+    private val abis: List<Abi> by lazy {
+        val abiProps = listOf(
+            "ro.product.cpu.abi",
+            "ro.product.cpu.abi2",
+            "ro.product.cpu.abilist",
+        )
+        val abiSet = mutableSetOf<Abi>()
+        for (abiProp in abiProps) {
+            for (abiName in getProp(abiProp).trim().split(",")) {
+                Abi.fromAbiName(abiName)?.let { abiSet.add(it) }
+            }
+        }
+        abiSet.toList().sortedBy { it.abiName }
+    }
+
+    private val version: Int by lazy {
+        getProp("ro.build.version.sdk").trim().toInt()
+    }
+
+    fun compatibleWith(abi: Abi, minSdkVersion: Int) =
+        abi in abis && minSdkVersion <= version
+
+    fun push(src: File, dest: File) =
+        run(listOf("push", src.toString(), dest.toString()))
+
+    fun shell(cmd: Iterable<String>) = run(listOf("shell") + cmd)
+
+    private fun getProp(name: String): String = shell(listOf("getprop", name))
+
+    private fun run(args: Iterable<String>): String = adb(args, serial)
+}
+
+class DeviceFleet {
+    private fun lineHasUsableDevice(line: String): Boolean {
+        if (line.isBlank()) {
+            return false
+        }
+        if (line == "List of devices attached") {
+            return false
+        }
+        if (line.contains("offline")) {
+            return false
+        }
+        if (line.contains("unauthorized")) {
+            return false
+        }
+        if (line.startsWith("* daemon")) {
+            return false
+        }
+        return true
+    }
+
+    private val devices: List<Device> by lazy {
+        adb(listOf("devices")).lines().filter { lineHasUsableDevice(it) }.map {
+            Device(it.split("\\s".toRegex()).first())
+        }
+    }
+
+    fun findDeviceFor(abi: Abi, minSdkVersion: Int): Device? =
+        devices.find { it.compatibleWith(abi, minSdkVersion) }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/android/ndkports/MesonPort.kt b/buildSrc/src/main/kotlin/com/android/ndkports/MesonPortTask.kt
similarity index 61%
rename from src/main/kotlin/com/android/ndkports/MesonPort.kt
rename to buildSrc/src/main/kotlin/com/android/ndkports/MesonPortTask.kt
index 8c1f706..6be9b9e 100644
--- a/src/main/kotlin/com/android/ndkports/MesonPort.kt
+++ b/buildSrc/src/main/kotlin/com/android/ndkports/MesonPortTask.kt
@@ -16,24 +16,41 @@
 
 package com.android.ndkports
 
+import org.gradle.api.model.ObjectFactory
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
 import java.io.File
+import javax.inject.Inject
 
-abstract class MesonPort : Port() {
+@Suppress("UnstableApiUsage")
+abstract class MesonPortTask @Inject constructor(objects: ObjectFactory) :
+    PortTask() {
     enum class DefaultLibraryType(val argument: String) {
-        Both("both"),
-        Shared("shared"),
-        Static("static")
+        Both("both"), Shared("shared"), Static("static")
     }
 
-    open val defaultLibraryType: DefaultLibraryType = DefaultLibraryType.Shared
+    @get:Input
+    val defaultLibraryType: Property<DefaultLibraryType> =
+        objects.property(DefaultLibraryType::class.java)
+            .convention(DefaultLibraryType.Shared)
 
-    override fun configure(
+    override fun buildForAbi(
         toolchain: Toolchain,
-        sourceDirectory: File,
+        workingDirectory: File,
         buildDirectory: File,
-        installDirectory: File,
-        workingDirectory: File
-    ): Result<Unit, String> {
+        installDirectory: File
+    ) {
+        configure(toolchain, workingDirectory, buildDirectory, installDirectory)
+        build(buildDirectory)
+        install(buildDirectory)
+    }
+
+    private fun configure(
+        toolchain: Toolchain,
+        workingDirectory: File,
+        buildDirectory: File,
+        installDirectory: File
+    ) {
         val cpuFamily = when (toolchain.abi) {
             Abi.Arm -> "arm"
             Abi.Arm64 -> "aarch64"
@@ -49,7 +66,8 @@
         }
 
         val crossFile = workingDirectory.resolve("cross_file.txt").apply {
-            writeText("""
+            writeText(
+                """
             [binaries]
             ar = '${toolchain.ar}'
             c = '${toolchain.clang}'
@@ -61,10 +79,11 @@
             cpu_family = '$cpuFamily'
             cpu = '$cpu'
             endian = 'little'
-            """.trimIndent())
+            """.trimIndent()
+            )
         }
 
-        return executeProcessStep(
+        executeSubprocess(
             listOf(
                 "meson",
                 "--cross-file",
@@ -74,23 +93,16 @@
                 "--prefix",
                 installDirectory.absolutePath,
                 "--default-library",
-                defaultLibraryType.argument,
-                sourceDirectory.absolutePath,
+                defaultLibraryType.get().argument,
+                sourceDirectory.get().asFile.absolutePath,
                 buildDirectory.absolutePath
             ), workingDirectory
         )
     }
 
-    override fun build(
-        toolchain: Toolchain,
-        buildDirectory: File
-    ): Result<Unit, String> =
-        executeProcessStep(listOf("ninja", "-v"), buildDirectory)
+    private fun build(buildDirectory: File) =
+        executeSubprocess(listOf("ninja", "-v"), buildDirectory)
 
-    override fun install(
-        toolchain: Toolchain,
-        buildDirectory: File,
-        installDirectory: File
-    ): Result<Unit, String> =
-        executeProcessStep(listOf("ninja", "-v", "install"), buildDirectory)
+    private fun install(buildDirectory: File) =
+        executeSubprocess(listOf("ninja", "-v", "install"), buildDirectory)
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/android/ndkports/Ndk.kt b/buildSrc/src/main/kotlin/com/android/ndkports/Ndk.kt
similarity index 94%
rename from src/main/kotlin/com/android/ndkports/Ndk.kt
rename to buildSrc/src/main/kotlin/com/android/ndkports/Ndk.kt
index af66cb5..7ccd255 100644
--- a/src/main/kotlin/com/android/ndkports/Ndk.kt
+++ b/buildSrc/src/main/kotlin/com/android/ndkports/Ndk.kt
@@ -17,7 +17,6 @@
 package com.android.ndkports
 
 import java.io.File
-import java.lang.RuntimeException
 
 class Ndk(val path: File) {
     val version = NdkVersion.fromNdk(path)
@@ -36,4 +35,5 @@
 
     private val toolchainDirectory = llvmBaseDir.resolve(hostTag)
     val toolchainBinDirectory = toolchainDirectory.resolve("bin")
+    val sysrootDirectory = toolchainDirectory.resolve("sysroot")
 }
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/com/android/ndkports/NdkPortsPlugin.kt b/buildSrc/src/main/kotlin/com/android/ndkports/NdkPortsPlugin.kt
new file mode 100644
index 0000000..ff629c0
--- /dev/null
+++ b/buildSrc/src/main/kotlin/com/android/ndkports/NdkPortsPlugin.kt
@@ -0,0 +1,179 @@
+package com.android.ndkports
+
+import org.gradle.api.InvalidUserDataException
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.attributes.Attribute
+import org.gradle.api.component.SoftwareComponentFactory
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.model.ObjectFactory
+import org.gradle.api.plugins.BasePlugin
+import org.gradle.api.provider.Property
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.bundling.Zip
+import javax.inject.Inject
+
+abstract class NdkPortsExtension {
+    abstract val source: RegularFileProperty
+
+    abstract val ndkPath: DirectoryProperty
+
+    abstract val minSdkVersion: Property<Int>
+}
+
+class NdkPortsPluginImpl(
+    private val project: Project,
+    private val softwareComponentFactory: SoftwareComponentFactory,
+    objects: ObjectFactory,
+) {
+    private val topBuildDir = project.buildDir.resolve("port")
+
+    private val extension =
+        project.extensions.create("ndkPorts", NdkPortsExtension::class.java)
+
+    private var portTaskAdded: Boolean = false
+    private val portTask = objects.property(PortTask::class.java)
+    private lateinit var prefabTask: Provider<PrefabTask>
+    private lateinit var extractTask: Provider<SourceExtractTask>
+    private lateinit var packageTask: Provider<PackageBuilderTask>
+    private lateinit var aarTask: Provider<Zip>
+
+    private lateinit var implementation: Configuration
+    private lateinit var exportedAars: Configuration
+    private lateinit var consumedAars: Configuration
+
+    private val artifactType = Attribute.of("artifactType", String::class.java)
+
+    private fun createConfigurations() {
+        implementation = project.configurations.create("implementation") {
+            it.isCanBeResolved = false
+            it.isCanBeConsumed = false
+        }
+
+        exportedAars = project.configurations.create("exportedAars") {
+            it.isCanBeResolved = false
+            it.isCanBeConsumed = true
+            it.extendsFrom(implementation)
+            it.attributes { attributes ->
+                with(attributes) {
+                    attribute(artifactType, "aar")
+                }
+            }
+        }
+
+        consumedAars = project.configurations.create("consumedAars") {
+            it.isCanBeResolved = true
+            it.isCanBeConsumed = false
+            it.extendsFrom(implementation)
+            it.attributes { attributes ->
+                with(attributes) {
+                    attribute(artifactType, "aar")
+                }
+            }
+        }
+    }
+
+    private fun createTasks() {
+        prefabTask = project.tasks.register("prefab", PrefabTask::class.java) {
+            with(it) {
+                aars = consumedAars.incoming.artifacts.artifactFiles
+                outputDirectory.set(topBuildDir.resolve("dependencies"))
+                ndkPath.set(extension.ndkPath)
+                minSdkVersion.set(extension.minSdkVersion)
+            }
+        }
+
+        extractTask = project.tasks.register(
+            "extractSrc", SourceExtractTask::class.java
+        ) {
+            with(it) {
+                source.set(extension.source)
+                outDir.set(topBuildDir.resolve("src"))
+            }
+        }
+
+        packageTask = project.tasks.register(
+            "prefabPackage", PackageBuilderTask::class.java
+        ) {
+            if (!portTask.isPresent) {
+                throw InvalidUserDataException(
+                    "The ndkports plugin was applied but no port task was " +
+                            "registered. A task deriving from NdkPortsTask " +
+                            "must be registered."
+                )
+            }
+            with(it) {
+                sourceDirectory.set(extractTask.get().outDir)
+                outDir.set(topBuildDir)
+                ndkPath.set(extension.ndkPath)
+                installDirectory.set(portTask.get().installDir)
+                minSdkVersion.set(extension.minSdkVersion)
+            }
+        }
+
+        aarTask = project.tasks.register("packageAar", Zip::class.java) {
+            it.from(packageTask.get().intermediatesDirectory)
+            it.archiveExtension.set("aar")
+            it.dependsOn(packageTask)
+        }
+
+        project.artifacts.add(exportedAars.name, aarTask)
+
+        val portTasks = project.tasks.withType(PortTask::class.java)
+        portTasks.whenTaskAdded { portTask ->
+            if (portTaskAdded) {
+                throw InvalidUserDataException(
+                    "Cannot define multiple port tasks for a single module"
+                )
+            }
+            portTaskAdded = true
+            this.portTask.set(portTask)
+
+            with (portTask) {
+                sourceDirectory.set(extractTask.get().outDir)
+                ndkPath.set(extension.ndkPath)
+                buildDir.set(topBuildDir)
+                minSdkVersion.set(extension.minSdkVersion)
+                prefabGenerated.set(prefabTask.get().generatedDirectory)
+            }
+        }
+
+        val testTasks =
+            project.tasks.withType(AndroidExecutableTestTask::class.java)
+        testTasks.whenTaskAdded { testTask ->
+            with (testTask) {
+                dependsOn(aarTask)
+                minSdkVersion.set(extension.minSdkVersion)
+                ndkPath.set(extension.ndkPath)
+            }
+            project.tasks.getByName("check").dependsOn(testTask)
+        }
+    }
+
+    private fun createComponents() {
+        val adhocComponent = softwareComponentFactory.adhoc("prefab")
+        project.components.add(adhocComponent)
+        adhocComponent.addVariantsFromConfiguration(exportedAars) {
+            it.mapToMavenScope("runtime")
+        }
+    }
+
+    fun apply() {
+        project.pluginManager.apply(BasePlugin::class.java)
+        createConfigurations()
+        createTasks()
+        createComponents()
+    }
+}
+
+@Suppress("UnstableApiUsage", "Unused")
+class NdkPortsPlugin @Inject constructor(
+    private val objects: ObjectFactory,
+    private val softwareComponentFactory: SoftwareComponentFactory,
+) : Plugin<Project> {
+    override fun apply(project: Project) {
+        NdkPortsPluginImpl(project, softwareComponentFactory, objects).apply()
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/android/ndkports/NdkVersion.kt b/buildSrc/src/main/kotlin/com/android/ndkports/NdkVersion.kt
similarity index 100%
rename from src/main/kotlin/com/android/ndkports/NdkVersion.kt
rename to buildSrc/src/main/kotlin/com/android/ndkports/NdkVersion.kt
diff --git a/buildSrc/src/main/kotlin/com/android/ndkports/PackageBuilderTask.kt b/buildSrc/src/main/kotlin/com/android/ndkports/PackageBuilderTask.kt
new file mode 100644
index 0000000..a7c408d
--- /dev/null
+++ b/buildSrc/src/main/kotlin/com/android/ndkports/PackageBuilderTask.kt
@@ -0,0 +1,134 @@
+package com.android.ndkports
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.NamedDomainObjectContainer
+import org.gradle.api.Project
+import org.gradle.api.file.Directory
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.model.ObjectFactory
+import org.gradle.api.provider.ListProperty
+import org.gradle.api.provider.MapProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.Nested
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.TaskAction
+import javax.inject.Inject
+
+abstract class ModuleProperty @Inject constructor(
+    objectFactory: ObjectFactory,
+    @get:Input val name: String,
+) {
+    @Suppress("UnstableApiUsage")
+    @get:Input
+    val static: Property<Boolean> =
+        objectFactory.property(Boolean::class.java).convention(false)
+
+    @Suppress("UnstableApiUsage")
+    @get:Input
+    val headerOnly: Property<Boolean> =
+        objectFactory.property(Boolean::class.java).convention(false)
+
+    @Suppress("UnstableApiUsage")
+    @get:Input
+    val includesPerAbi: Property<Boolean> =
+        objectFactory.property(Boolean::class.java).convention(false)
+
+    @Suppress("UnstableApiUsage")
+    @get:Input
+    val dependencies: ListProperty<String> =
+        objectFactory.listProperty(String::class.java).convention(emptyList())
+}
+
+abstract class PackageBuilderTask @Inject constructor(
+    objectFactory: ObjectFactory,
+) : DefaultTask() {
+    /**
+     * The name of the port. Will be used as the package name in prefab.json.
+     */
+    @Suppress("UnstableApiUsage")
+    @get:Input
+    val packageName: Property<String> =
+        objectFactory.property(String::class.java).convention(project.name)
+
+    /**
+     * The version to encode in the prefab.json.
+     *
+     * This version must be compatible with CMake's `find_package` for
+     * config-style packages. This means that it must be one to four decimal
+     * separated integers. No other format is allowed.
+     *
+     * If not set, the default is [Project.getVersion] as interpreted by
+     * [CMakeCompatibleVersion.parse].
+     *
+     * For example, OpenSSL 1.1.1g will set this value to
+     * `CMakeCompatibleVersion(1, 1, 1, 7)`.
+     */
+    @get:Input
+    abstract val version: Property<CMakeCompatibleVersion>
+
+    @get:Input
+    abstract val minSdkVersion: Property<Int>
+
+    @get:Nested
+    abstract val modules: NamedDomainObjectContainer<ModuleProperty>
+
+    @Suppress("UnstableApiUsage")
+    @get:Input
+    val licensePath: Property<String> =
+        objectFactory.property(String::class.java).convention("LICENSE")
+
+    @Suppress("UnstableApiUsage")
+    @get:Input
+    abstract val dependencies: MapProperty<String, String>
+
+    @get:InputDirectory
+    abstract val sourceDirectory: DirectoryProperty
+
+    @get:InputDirectory
+    abstract val installDirectory: DirectoryProperty
+
+    @get:Internal
+    abstract val outDir: DirectoryProperty
+
+    @get:OutputDirectory
+    val intermediatesDirectory: Provider<Directory>
+        get() = outDir.dir("aar")
+
+    @get:InputDirectory
+    abstract val ndkPath: DirectoryProperty
+
+    private val ndk: Ndk
+        get() = Ndk(ndkPath.asFile.get())
+
+    @TaskAction
+    fun run() {
+        val modules = modules.asMap.values.map {
+            ModuleDescription(
+                it.name,
+                it.static.get(),
+                it.headerOnly.get(),
+                it.includesPerAbi.get(),
+                it.dependencies.get()
+            )
+        }
+        PrefabPackageBuilder(
+            PackageData(
+                packageName.get(),
+                project.version as String,
+                version.get(),
+                minSdkVersion.get(),
+                licensePath.get(),
+                modules,
+                dependencies.get(),
+            ),
+            intermediatesDirectory.get().asFile,
+            installDirectory.get().asFile,
+            sourceDirectory.get().asFile,
+            ndk,
+        ).build()
+    }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/com/android/ndkports/PortTask.kt b/buildSrc/src/main/kotlin/com/android/ndkports/PortTask.kt
new file mode 100644
index 0000000..223afb0
--- /dev/null
+++ b/buildSrc/src/main/kotlin/com/android/ndkports/PortTask.kt
@@ -0,0 +1,94 @@
+package com.android.ndkports
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.Directory
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+
+@Suppress("UnstableApiUsage")
+abstract class PortTask : DefaultTask() {
+
+    @get:InputDirectory
+    abstract val sourceDirectory: DirectoryProperty
+
+    @get:OutputDirectory
+    abstract val buildDir: DirectoryProperty
+
+    @get:OutputDirectory
+    val installDir: Provider<Directory>
+        get() = buildDir.dir("install")
+
+    @get:InputDirectory
+    abstract val prefabGenerated: DirectoryProperty
+
+    @get:Input
+    abstract val minSdkVersion: Property<Int>
+
+    @get:InputDirectory
+    abstract val ndkPath: DirectoryProperty
+
+    private val ndk: Ndk
+        get() = Ndk(ndkPath.asFile.get())
+
+    /**
+     * The number of CPUs available for building.
+     *
+     * May be passed to the build system if required.
+     */
+    @Internal
+    protected val ncpus = Runtime.getRuntime().availableProcessors()
+
+    protected fun executeSubprocess(
+        args: List<String>,
+        workingDirectory: File,
+        additionalEnvironment: Map<String, String>? = null
+    ) {
+        val pb = ProcessBuilder(args).redirectErrorStream(true)
+            .directory(workingDirectory)
+
+        if (additionalEnvironment != null) {
+            pb.environment().putAll(additionalEnvironment)
+        }
+
+        val result = pb.start()
+        val output = result.inputStream.bufferedReader().use { it.readText() }
+        if (result.waitFor() != 0) {
+            throw RuntimeException("Subprocess failed with:\n$output")
+        }
+    }
+
+    @Suppress("MemberVisibilityCanBePrivate")
+    fun buildDirectoryFor(abi: Abi): File =
+        buildDir.asFile.get().resolve("build/$abi")
+
+    @Suppress("MemberVisibilityCanBePrivate")
+    fun installDirectoryFor(abi: Abi): File =
+        installDir.get().asFile.resolve("$abi")
+
+    @TaskAction
+    fun run() {
+        for (abi in Abi.values()) {
+            val api = abi.adjustMinSdkVersion(minSdkVersion.get())
+            buildForAbi(
+                Toolchain(ndk, abi, api),
+                buildDir.asFile.get(),
+                buildDirectory = buildDirectoryFor(abi),
+                installDirectory = installDirectoryFor(abi),
+            )
+        }
+    }
+
+    abstract fun buildForAbi(
+        toolchain: Toolchain,
+        workingDirectory: File,
+        buildDirectory: File,
+        installDirectory: File
+    )
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/com/android/ndkports/PrefabPackageBuilder.kt b/buildSrc/src/main/kotlin/com/android/ndkports/PrefabPackageBuilder.kt
new file mode 100644
index 0000000..27be792
--- /dev/null
+++ b/buildSrc/src/main/kotlin/com/android/ndkports/PrefabPackageBuilder.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ndkports
+
+import com.google.prefab.api.AndroidAbiMetadata
+import com.google.prefab.api.ModuleMetadataV1
+import com.google.prefab.api.PackageMetadataV1
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import org.redundent.kotlin.xml.xml
+import java.io.File
+import java.io.Serializable
+
+data class PackageData(
+    val name: String,
+    val mavenVersion: String,
+    val prefabVersion: CMakeCompatibleVersion,
+    val minSdkVersion: Int,
+    val licensePath: String,
+    val modules: List<ModuleDescription>,
+    val dependencies: Map<String, String>,
+)
+
+/**
+ * A module exported by the package.
+ *
+ * As currently implemented by ndkports, one module is exactly one library.
+ * Prefab supports header-only libraries, but ndkports does not support these
+ * yet.
+ *
+ * Static libraries are not currently supported by ndkports.
+ *
+ * @property[name] The name of the module. Note that currently the name of the
+ * installed library file must be exactly `lib$name.so`.
+ * @property[includesPerAbi] Set to true if a different set of headers should be
+ * exposed per-ABI. Not currently implemented.
+ * @property[dependencies] A list of other modules required by this module, in
+ * the format described by https://google.github.io/prefab/.
+ */
+data class ModuleDescription(
+    val name: String,
+    val static: Boolean,
+    val headerOnly: Boolean,
+    val includesPerAbi: Boolean,
+    val dependencies: List<String>,
+) : Serializable
+
+class PrefabPackageBuilder(
+    private val packageData: PackageData,
+    private val packageDirectory: File,
+    private val directory: File,
+    private val sourceDirectory: File,
+    private val ndk: Ndk,
+) {
+    private val prefabDirectory = packageDirectory.resolve("prefab")
+    private val modulesDirectory = prefabDirectory.resolve("modules")
+
+    // TODO: Get from gradle.
+    private val packageName = "com.android.ndk.thirdparty.${packageData.name}"
+
+    private fun preparePackageDirectory() {
+        if (packageDirectory.exists()) {
+            packageDirectory.deleteRecursively()
+        }
+        modulesDirectory.mkdirs()
+    }
+
+    private fun makePackageMetadata() {
+        prefabDirectory.resolve("prefab.json").writeText(
+            Json.encodeToString(
+                PackageMetadataV1(
+                    packageData.name,
+                    schemaVersion = 1,
+                    dependencies = packageData.dependencies.keys.toList(),
+                    version = packageData.prefabVersion.toString()
+                )
+            )
+        )
+    }
+
+    private fun makeModuleMetadata(module: ModuleDescription, moduleDirectory: File) {
+        moduleDirectory.resolve("module.json").writeText(
+            Json.encodeToString(
+                ModuleMetadataV1(
+                    exportLibraries = module.dependencies
+                )
+            )
+        )
+    }
+
+    private fun installLibForAbi(module: ModuleDescription, abi: Abi, libsDir: File) {
+        val extension = if (module.static) "a" else "so"
+        val libName = "lib${module.name}.${extension}"
+        val installDirectory = libsDir.resolve("android.${abi.abiName}").apply {
+            mkdirs()
+        }
+
+        directory.resolve("$abi/lib/$libName")
+            .copyTo(installDirectory.resolve(libName))
+
+        installDirectory.resolve("abi.json").writeText(
+            Json.encodeToString(
+                AndroidAbiMetadata(
+                    abi = abi.abiName,
+                    api = abi.adjustMinSdkVersion(packageData.minSdkVersion),
+                    ndk = ndk.version.major,
+                    stl = "c++_shared"
+                )
+            )
+        )
+    }
+
+    private fun installLicense() {
+        val src = sourceDirectory.resolve(packageData.licensePath)
+        val dest = packageDirectory.resolve("META-INF")
+            .resolve(File(packageData.licensePath).name)
+        src.copyTo(dest)
+    }
+
+    private fun createAndroidManifest() {
+        packageDirectory.resolve("AndroidManifest.xml")
+            .writeText(xml("manifest") {
+                attributes(
+                    "xmlns:android" to "http://schemas.android.com/apk/res/android",
+                    "package" to packageName,
+                    "android:versionCode" to 1,
+                    "android:versionName" to "1.0"
+                )
+
+                "uses-sdk" {
+                    attributes(
+                        "android:minSdkVersion" to packageData.minSdkVersion,
+                        "android:targetSdkVersion" to 29
+                    )
+                }
+            }.toString())
+    }
+
+    fun build() {
+        preparePackageDirectory()
+        makePackageMetadata()
+        for (module in packageData.modules) {
+            val moduleDirectory = modulesDirectory.resolve(module.name).apply {
+                mkdirs()
+            }
+
+            makeModuleMetadata(module, moduleDirectory)
+
+            if (module.includesPerAbi) {
+                TODO()
+            } else {
+                // TODO: Check that headers are actually identical across ABIs.
+                directory.resolve("${Abi.Arm}/include")
+                    .copyRecursively(moduleDirectory.resolve("include"))
+            }
+
+            if (!module.headerOnly) {
+                val libsDir = moduleDirectory.resolve("libs").apply { mkdirs() }
+                for (abi in Abi.values()) {
+                    installLibForAbi(module, abi, libsDir)
+                }
+            }
+        }
+
+        installLicense()
+
+        createAndroidManifest()
+    }
+}
diff --git a/buildSrc/src/main/kotlin/com/android/ndkports/PrefabSysrootPlugin.kt b/buildSrc/src/main/kotlin/com/android/ndkports/PrefabSysrootPlugin.kt
new file mode 100644
index 0000000..2828764
--- /dev/null
+++ b/buildSrc/src/main/kotlin/com/android/ndkports/PrefabSysrootPlugin.kt
@@ -0,0 +1,60 @@
+package com.android.ndkports
+
+import com.google.prefab.api.BuildSystemInterface
+import com.google.prefab.api.Module
+import com.google.prefab.api.Package
+import com.google.prefab.api.PlatformDataInterface
+import java.io.File
+
+class PrefabSysrootPlugin(
+    override val outputDirectory: File, override val packages: List<Package>
+) : BuildSystemInterface {
+
+    override fun generate(requirements: Collection<PlatformDataInterface>) {
+        prepareOutputDirectory(outputDirectory)
+
+        for (pkg in packages) {
+            for (module in pkg.modules) {
+                for (requirement in requirements) {
+                    installModule(module, requirement)
+                }
+            }
+        }
+    }
+
+    private fun installModule(
+        module: Module, requirement: PlatformDataInterface
+    ) {
+        val installDir = outputDirectory.resolve(requirement.targetTriple)
+        val includeDir = installDir.resolve("include")
+
+        if (module.isHeaderOnly) {
+            installHeaders(module.includePath.toFile(), includeDir)
+            return
+        }
+
+        val library = module.getLibraryFor(requirement)
+        installHeaders(module.includePath.toFile(), includeDir)
+        val libDir = installDir.resolve("lib").apply {
+            mkdirs()
+        }
+        library.path.toFile().apply { copyTo(libDir.resolve(name)) }
+    }
+
+    private fun installHeaders(src: File, dest: File) {
+        src.copyRecursively(dest) { file, exception ->
+            if (exception !is FileAlreadyExistsException) {
+                throw exception
+            }
+
+            if (!file.readBytes().contentEquals(exception.file.readBytes())) {
+                val path = file.relativeTo(dest)
+                throw RuntimeException(
+                    "Found duplicate headers with non-equal contents: $path"
+                )
+            }
+
+            OnErrorAction.SKIP
+        }
+    }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/com/android/ndkports/PrefabTask.kt b/buildSrc/src/main/kotlin/com/android/ndkports/PrefabTask.kt
new file mode 100644
index 0000000..d58f1fa
--- /dev/null
+++ b/buildSrc/src/main/kotlin/com/android/ndkports/PrefabTask.kt
@@ -0,0 +1,95 @@
+package com.android.ndkports
+
+import com.google.prefab.api.Android
+import com.google.prefab.api.BuildSystemInterface
+import com.google.prefab.api.Package
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.Directory
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.FileCollection
+import org.gradle.api.provider.Property
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Optional
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+import java.util.zip.ZipFile
+
+abstract class PrefabTask : DefaultTask() {
+    @InputFiles
+    lateinit var aars: FileCollection
+
+    @get:OutputDirectory
+    abstract val outputDirectory: DirectoryProperty
+
+    @get:OutputDirectory
+    val generatedDirectory: Provider<Directory>
+        get() = outputDirectory.dir("generated")
+
+    @get:Optional
+    @get:Input
+    abstract val generator: Property<Class<out BuildSystemInterface>>
+
+    @get:InputDirectory
+    abstract val ndkPath: DirectoryProperty
+
+    @get:Input
+    abstract val minSdkVersion: Property<Int>
+
+    private val ndk: Ndk
+        get() = Ndk(ndkPath.asFile.get())
+
+    @TaskAction
+    fun run() {
+        if (!generator.isPresent) {
+            // Creating the generated directory even if we have no generator
+            // makes it easier to write tasks that *might* consume prefab
+            // packages.
+            generatedDirectory.get().asFile.mkdirs()
+            return
+        }
+
+        val outDir = outputDirectory.get().asFile
+        val packages = mutableListOf<Package>()
+        for (aar in aars) {
+            val packagePath = outDir.resolve(aar.nameWithoutExtension)
+            extract(aar, packagePath)
+            packages.add(Package(packagePath.toPath().resolve("prefab")))
+        }
+        generateSysroot(packages, minSdkVersion.get(), ndk.version.major)
+    }
+
+    private fun extract(aar: File, extractDir: File) {
+        ZipFile(aar).use { zip ->
+            zip.entries().asSequence().forEach { entry ->
+                zip.getInputStream(entry).use { input ->
+                    val outFile = extractDir.resolve(entry.name)
+                    if (entry.isDirectory) {
+                        outFile.mkdirs()
+                    } else {
+                        outFile.outputStream().use { output ->
+                            input.copyTo(output)
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private fun generateSysroot(
+        packages: List<Package>, osVersion: Int, ndkVersion: Int
+    ) {
+        val generatorType = generator.get()
+        val constructor =
+            generatorType.getConstructor(File::class.java, List::class.java)
+        val buildSystemIntegration =
+            constructor.newInstance(generatedDirectory.get().asFile, packages)
+
+        buildSystemIntegration.generate(Android.Abi.values().map {
+            Android(it, osVersion, Android.Stl.CxxShared, ndkVersion)
+        })
+    }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/com/android/ndkports/SourceExtractTask.kt b/buildSrc/src/main/kotlin/com/android/ndkports/SourceExtractTask.kt
new file mode 100644
index 0000000..4f2660f
--- /dev/null
+++ b/buildSrc/src/main/kotlin/com/android/ndkports/SourceExtractTask.kt
@@ -0,0 +1,34 @@
+package com.android.ndkports
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.TaskAction
+
+abstract class SourceExtractTask : DefaultTask() {
+    @get:InputFile
+    abstract val source: RegularFileProperty
+
+    @get:OutputDirectory
+    abstract val outDir: DirectoryProperty
+
+    @TaskAction
+    fun run() {
+        val pb = ProcessBuilder(
+            listOf(
+                "tar",
+                "xf",
+                source.get().asFile.absolutePath,
+                "--strip-components=1"
+            )
+        ).redirectErrorStream(true).directory(outDir.get().asFile)
+
+        val result = pb.start()
+        val output = result.inputStream.bufferedReader().use { it.readText() }
+        if (result.waitFor() != 0) {
+            throw RuntimeException("Subprocess failed with:\n$output")
+        }
+    }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/com/android/ndkports/Testing.kt b/buildSrc/src/main/kotlin/com/android/ndkports/Testing.kt
new file mode 100644
index 0000000..203e04f
--- /dev/null
+++ b/buildSrc/src/main/kotlin/com/android/ndkports/Testing.kt
@@ -0,0 +1,151 @@
+package com.android.ndkports
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.SendChannel
+import kotlinx.coroutines.channels.toList
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+
+sealed class TestResult(val name: String, val abi: Abi) {
+    class Success(name: String, abi: Abi) : TestResult(name, abi) {
+        override fun toString(): String = "PASS $abi $name"
+    }
+
+    class Failure(name: String, abi: Abi, private val output: String) :
+        TestResult(name, abi) {
+        override fun toString(): String = "FAIL $abi $name: $output"
+    }
+}
+
+private val BASE_DEVICE_DIRECTORY = File("/data/local/tmp/ndkports")
+
+data class PushSpec(val src: File, val dest: File)
+
+class PushBuilder(val abi: Abi, val toolchain: Toolchain) {
+    val pushSpecs = mutableListOf<PushSpec>()
+
+    fun push(src: File, dest: File) = pushSpecs.add(PushSpec(src, dest))
+}
+
+data class ShellTestSpec(val name: String, val cmd: Iterable<String>)
+
+class ShellTestBuilder(val deviceDirectory: File, val abi: Abi) {
+    val runSpecs = mutableListOf<ShellTestSpec>()
+
+    fun shellTest(name: String, cmd: Iterable<String>) =
+        runSpecs.add(ShellTestSpec(name, cmd))
+}
+
+abstract class AndroidExecutableTestTask : DefaultTask() {
+    @get:InputDirectory
+    abstract val ndkPath: DirectoryProperty
+
+    private val ndk: Ndk
+        get() = Ndk(ndkPath.asFile.get())
+
+    @get:Input
+    abstract val minSdkVersion: Property<Int>
+
+    @get:Input
+    abstract val push: Property<PushBuilder.() -> Unit>
+
+    fun push(block: PushBuilder.() -> Unit) = push.set(block)
+
+    @get:Input
+    abstract val run: Property<ShellTestBuilder.() -> Unit>
+
+    fun run(block: ShellTestBuilder.() -> Unit) = run.set(block)
+
+    private fun deviceDirectoryForAbi(abi: Abi): File =
+        BASE_DEVICE_DIRECTORY.resolve(project.name).resolve(abi.toString())
+
+    private suspend fun runTests(
+        device: Device, abi: Abi, resultChannel: SendChannel<TestResult>
+    ) = coroutineScope {
+        val deviceDirectory = deviceDirectoryForAbi(abi)
+
+        val pushBlock = push.get()
+        val runBlock = run.get()
+
+        val pushBuilder =
+            PushBuilder(abi, Toolchain(ndk, abi, minSdkVersion.get()))
+        pushBuilder.pushBlock()
+        coroutineScope {
+            pushBuilder.pushSpecs.forEach {
+                launch(Dispatchers.IO) {
+                    device.push(
+                        it.src, deviceDirectory.resolve(it.dest)
+                    )
+                }
+            }
+        }
+
+        val runBuilder = ShellTestBuilder(deviceDirectory, abi)
+        runBuilder.runBlock()
+        runBuilder.runSpecs.forEach {
+            launch(Dispatchers.IO) {
+                val result = try {
+                    device.shell(it.cmd)
+                    TestResult.Success(it.name, abi)
+                } catch (ex: AdbException) {
+                    TestResult.Failure(it.name, abi, "${ex.cmd}\n${ex.output}")
+                }
+
+                resultChannel.send(result)
+            }
+        }
+    }
+
+    @Suppress("UnstableApiUsage")
+    @TaskAction
+    fun runTask() = runBlocking {
+        val fleet = DeviceFleet()
+        val warningChannel = Channel<String>(Channel.UNLIMITED)
+        val resultChannel = Channel<TestResult>(Channel.UNLIMITED)
+        coroutineScope {
+            for (abi in Abi.values()) {
+                launch {
+                    val device = fleet.findDeviceFor(
+                        abi, abi.adjustMinSdkVersion(minSdkVersion.get())
+                    )
+                    if (device == null) {
+                        warningChannel.send(
+                            "No device capable of running tests for $abi " +
+                                    "minSdkVersion 21"
+                        )
+                        return@launch
+                    }
+                    device.shell(
+                        listOf(
+                            "rm", "-rf", deviceDirectoryForAbi(abi).toString()
+                        )
+                    )
+                    runTests(device, abi, resultChannel)
+                }
+            }
+        }
+        warningChannel.close()
+        resultChannel.close()
+
+        for (warning in warningChannel) {
+            logger.warn(warning)
+        }
+
+        val failures =
+            resultChannel.toList().filterIsInstance<TestResult.Failure>()
+        if (failures.isNotEmpty()) {
+            throw RuntimeException(
+                "Tests failed:\n${failures.joinToString("\n")}"
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/android/ndkports/Toolchain.kt b/buildSrc/src/main/kotlin/com/android/ndkports/Toolchain.kt
similarity index 94%
rename from src/main/kotlin/com/android/ndkports/Toolchain.kt
rename to buildSrc/src/main/kotlin/com/android/ndkports/Toolchain.kt
index d42e8d1..1293ecb 100644
--- a/src/main/kotlin/com/android/ndkports/Toolchain.kt
+++ b/buildSrc/src/main/kotlin/com/android/ndkports/Toolchain.kt
@@ -29,6 +29,8 @@
         else -> "$binutilsTriple$api"
     }
 
+    val sysrootLibs = ndk.sysrootDirectory.resolve("usr/lib/$binutilsTriple")
+
     val binDir = ndk.toolchainBinDirectory
     val ar = binDir.resolve("$binutilsTriple-ar")
     val clang = binDir.resolve("$clangTriple-clang")
diff --git a/src/test/kotlin/com/android/ndkports/CMakeCompatibleVersionTest.kt b/buildSrc/src/test/kotlin/com/android/ndkports/CMakeCompatibleVersionTest.kt
similarity index 100%
rename from src/test/kotlin/com/android/ndkports/CMakeCompatibleVersionTest.kt
rename to buildSrc/src/test/kotlin/com/android/ndkports/CMakeCompatibleVersionTest.kt
diff --git a/src/test/kotlin/com/android/ndkports/NdkVersionTest.kt b/buildSrc/src/test/kotlin/com/android/ndkports/NdkVersionTest.kt
similarity index 100%
rename from src/test/kotlin/com/android/ndkports/NdkVersionTest.kt
rename to buildSrc/src/test/kotlin/com/android/ndkports/NdkVersionTest.kt
diff --git a/src/test/resources/junit-platform.properties b/buildSrc/src/test/resources/junit-platform.properties
similarity index 100%
rename from src/test/resources/junit-platform.properties
rename to buildSrc/src/test/resources/junit-platform.properties
diff --git a/curl/build.gradle.kts b/curl/build.gradle.kts
new file mode 100644
index 0000000..089ef04
--- /dev/null
+++ b/curl/build.gradle.kts
@@ -0,0 +1,106 @@
+import com.android.ndkports.AutoconfPortTask
+import com.android.ndkports.CMakeCompatibleVersion
+import com.android.ndkports.PrefabSysrootPlugin
+
+val portVersion = "7.79.1"
+
+group = "com.android.ndk.thirdparty"
+version = "$portVersion${rootProject.extra.get("snapshotSuffix")}"
+
+plugins {
+    id("maven-publish")
+    id("com.android.ndkports.NdkPorts")
+}
+
+dependencies {
+    implementation(project(":openssl"))
+}
+
+ndkPorts {
+    ndkPath.set(File(project.findProperty("ndkPath") as String))
+    source.set(project.file("src.tar.gz"))
+    minSdkVersion.set(16)
+}
+
+tasks.prefab {
+    generator.set(PrefabSysrootPlugin::class.java)
+}
+
+tasks.register<AutoconfPortTask>("buildPort") {
+    autoconf {
+        args(
+            "--disable-ntlm-wb",
+            "--enable-ipv6",
+            "--with-zlib",
+            "--with-ca-path=/system/etc/security/cacerts",
+            "--with-ssl=$sysroot"
+        )
+
+        // aarch64 still defaults to bfd which transitively checks libraries.
+        // When curl is linking one of its own libraries which depends on
+        // openssl, it doesn't pass -rpath-link to be able to find the SSL
+        // libraries and fails to build because of it.
+        //
+        // TODO: Switch to lld once we're using r21.
+        env("LDFLAGS", "-fuse-ld=gold")
+    }
+}
+
+tasks.prefabPackage {
+    version.set(CMakeCompatibleVersion.parse(portVersion))
+
+    licensePath.set("COPYING")
+
+    @Suppress("UnstableApiUsage") dependencies.set(
+        mapOf(
+            "openssl" to "1.1.1k"
+        )
+    )
+
+    modules {
+        create("curl") {
+            dependencies.set(
+                listOf(
+                    "//openssl:crypto", "//openssl:ssl"
+                )
+            )
+        }
+    }
+}
+
+publishing {
+    publications {
+        create<MavenPublication>("maven") {
+            from(components["prefab"])
+            pom {
+                name.set("curl")
+                description.set("The ndkports AAR for curl.")
+                url.set(
+                    "https://android.googlesource.com/platform/tools/ndkports"
+                )
+                licenses {
+                    license {
+                        name.set("The curl License")
+                        url.set("https://curl.haxx.se/docs/copyright.html")
+                        distribution.set("repo")
+                    }
+                }
+                developers {
+                    developer {
+                        name.set("The Android Open Source Project")
+                    }
+                }
+                scm {
+                    url.set("https://android.googlesource.com/platform/tools/ndkports")
+                    connection.set("scm:git:https://android.googlesource.com/platform/tools/ndkports")
+                }
+            }
+        }
+    }
+
+    repositories {
+        maven {
+            url = uri("${rootProject.buildDir}/repository")
+        }
+    }
+}
diff --git a/curl/src.tar.gz b/curl/src.tar.gz
new file mode 100644
index 0000000..b6d3b85
--- /dev/null
+++ b/curl/src.tar.gz
Binary files differ
diff --git a/googletest/build.gradle.kts b/googletest/build.gradle.kts
new file mode 100644
index 0000000..c365519
--- /dev/null
+++ b/googletest/build.gradle.kts
@@ -0,0 +1,124 @@
+import com.android.ndkports.AndroidExecutableTestTask
+import com.android.ndkports.CMakeCompatibleVersion
+import com.android.ndkports.CMakePortTask
+
+val portVersion = "1.11.0"
+
+group = "com.android.ndk.thirdparty"
+version = "$portVersion${rootProject.extra.get("snapshotSuffix")}"
+
+plugins {
+    id("maven-publish")
+    id("com.android.ndkports.NdkPorts")
+}
+
+ndkPorts {
+    ndkPath.set(File(project.findProperty("ndkPath") as String))
+    source.set(project.file("src.tar.gz"))
+    minSdkVersion.set(16)
+}
+
+val buildTask = tasks.register<CMakePortTask>("buildPort") {
+    cmake {
+        arg("-Dgtest_build_tests=ON")
+        arg("-Dgmock_build_tests=ON")
+    }
+}
+
+tasks.prefabPackage {
+    version.set(CMakeCompatibleVersion.parse(portVersion))
+
+    modules {
+        create("gtest") {
+            static.set(true)
+        }
+        create("gtest_main") {
+            static.set(true)
+        }
+        create("gmock") {
+            static.set(true)
+        }
+        create("gmock_main") {
+            static.set(true)
+        }
+    }
+}
+
+fun findTests(directory: File) = directory.listFiles()!!.filter {
+    // There are also many tests that end with test_, but those require running
+    // Python on the device.
+    it.name.endsWith("test")
+}
+
+tasks.register<AndroidExecutableTestTask>("test") {
+    push {
+        val buildDir = buildTask.get().buildDirectoryFor(abi)
+        findTests(buildDir.resolve("googlemock")).forEach { test ->
+            push(test, File("googlemock").resolve(test.name))
+        }
+        findTests(buildDir.resolve("googletest")).forEach { test ->
+            push(test, File("googletest").resolve(test.name))
+        }
+    }
+
+    run {
+        val buildDir = buildTask.get().buildDirectoryFor(abi)
+        findTests(buildDir.resolve("googlemock")).forEach { test ->
+            shellTest(
+                test.name, listOf(
+                    "cd",
+                    deviceDirectory.resolve("googlemock").toString(),
+                    "&&",
+                    "./${test.name}"
+                )
+            )
+        }
+        findTests(buildDir.resolve("googletest")).forEach { test ->
+            shellTest(
+                test.name, listOf(
+                    "cd",
+                    deviceDirectory.resolve("googletest").toString(),
+                    "&&",
+                    "./${test.name}"
+                )
+            )
+        }
+    }
+}
+
+publishing {
+    publications {
+        create<MavenPublication>("maven") {
+            from(components["prefab"])
+            pom {
+                name.set("GoogleTest")
+                description.set("The ndkports AAR for GoogleTest.")
+                url.set(
+                    "https://android.googlesource.com/platform/tools/ndkports"
+                )
+                licenses {
+                    license {
+                        name.set("BSD-3-Clause License")
+                        url.set("https://github.com/google/googletest/blob/master/LICENSE")
+                        distribution.set("repo")
+                    }
+                }
+                developers {
+                    developer {
+                        name.set("The Android Open Source Project")
+                    }
+                }
+                scm {
+                    url.set("https://android.googlesource.com/platform/tools/ndkports")
+                    connection.set("scm:git:https://android.googlesource.com/platform/tools/ndkports")
+                }
+            }
+        }
+    }
+
+    repositories {
+        maven {
+            url = uri("${rootProject.buildDir}/repository")
+        }
+    }
+}
diff --git a/googletest/src.tar.gz b/googletest/src.tar.gz
new file mode 100644
index 0000000..8163d74
--- /dev/null
+++ b/googletest/src.tar.gz
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 94336fc..e708b1c 100644
--- a/gradle/wrapper/gradle-wrapper.jar
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 7c4388a..da9702f 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index cccdd3d..4f906e0 100755
--- a/gradlew
+++ b/gradlew
@@ -1,5 +1,21 @@
 #!/usr/bin/env sh
 
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
 ##############################################################################
 ##
 ##  Gradle start up script for UN*X
@@ -28,7 +44,7 @@
 APP_BASE_NAME=`basename "$0"`
 
 # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
 
 # Use the maximum available, or set MAX_FD != -1 to use that value.
 MAX_FD="maximum"
@@ -66,6 +82,7 @@
 
 CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
 
+
 # Determine the Java command to use to start the JVM.
 if [ -n "$JAVA_HOME" ] ; then
     if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -109,10 +126,11 @@
     GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
 fi
 
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
     APP_HOME=`cygpath --path --mixed "$APP_HOME"`
     CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
     JAVACMD=`cygpath --unix "$JAVACMD"`
 
     # We build the pattern for arguments to be converted via cygpath
@@ -138,19 +156,19 @@
         else
             eval `echo args$i`="\"$arg\""
         fi
-        i=$((i+1))
+        i=`expr $i + 1`
     done
     case $i in
-        (0) set -- ;;
-        (1) set -- "$args0" ;;
-        (2) set -- "$args0" "$args1" ;;
-        (3) set -- "$args0" "$args1" "$args2" ;;
-        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
-        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
-        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
-        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
-        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
-        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+        0) set -- ;;
+        1) set -- "$args0" ;;
+        2) set -- "$args0" "$args1" ;;
+        3) set -- "$args0" "$args1" "$args2" ;;
+        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
     esac
 fi
 
@@ -159,14 +177,9 @@
     for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
     echo " "
 }
-APP_ARGS=$(save "$@")
+APP_ARGS=`save "$@"`
 
 # Collect all arguments for the java command, following the shell quoting and substitution rules
 eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
 
-# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
-if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
-  cd "$(dirname "$0")"
-fi
-
 exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index f955316..107acd3 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -1,3 +1,19 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
 @if "%DEBUG%" == "" @echo off
 @rem ##########################################################################
 @rem
@@ -13,15 +29,18 @@
 set APP_BASE_NAME=%~n0
 set APP_HOME=%DIRNAME%
 
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
 @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
 
 @rem Find java.exe
 if defined JAVA_HOME goto findJavaFromJavaHome
 
 set JAVA_EXE=java.exe
 %JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
+if "%ERRORLEVEL%" == "0" goto execute
 
 echo.
 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -35,7 +54,7 @@
 set JAVA_HOME=%JAVA_HOME:"=%
 set JAVA_EXE=%JAVA_HOME%/bin/java.exe
 
-if exist "%JAVA_EXE%" goto init
+if exist "%JAVA_EXE%" goto execute
 
 echo.
 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -45,28 +64,14 @@
 
 goto fail
 
-:init
-@rem Get command-line arguments, handling Windows variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-
 :execute
 @rem Setup the command line
 
 set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
 
+
 @rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
 
 :end
 @rem End local scope for the variables with windows NT shell
diff --git a/jsoncpp/build.gradle.kts b/jsoncpp/build.gradle.kts
new file mode 100644
index 0000000..29ffc66
--- /dev/null
+++ b/jsoncpp/build.gradle.kts
@@ -0,0 +1,103 @@
+import com.android.ndkports.AndroidExecutableTestTask
+import com.android.ndkports.CMakeCompatibleVersion
+import com.android.ndkports.MesonPortTask
+
+val portVersion = "1.9.5"
+
+group = "com.android.ndk.thirdparty"
+version = "$portVersion${rootProject.extra.get("snapshotSuffix")}"
+
+plugins {
+    id("maven-publish")
+    id("com.android.ndkports.NdkPorts")
+}
+
+ndkPorts {
+    ndkPath.set(File(project.findProperty("ndkPath") as String))
+    source.set(project.file("src.tar.gz"))
+    minSdkVersion.set(16)
+}
+
+tasks.extractSrc {
+    doLast {
+        // jsoncpp has a "version" file on the include path that conflicts with
+        // https://en.cppreference.com/w/cpp/header/version. Remove it so we can
+        // build.
+        outDir.get().asFile.resolve("version").delete()
+    }
+}
+
+val buildTask = tasks.register<MesonPortTask>("buildPort")
+
+tasks.prefabPackage {
+    version.set(CMakeCompatibleVersion.parse(portVersion))
+
+    modules {
+        create("jsoncpp")
+    }
+}
+
+tasks.register<AndroidExecutableTestTask>("test") {
+    push {
+        push(
+            buildTask.get().buildDirectoryFor(abi).resolve("jsoncpp_test"),
+            File("jsoncpp_test")
+        )
+        push(
+            buildTask.get().installDirectoryFor(abi)
+                .resolve("lib/libjsoncpp.so"), File("libjsoncpp.so")
+        )
+        push(
+            toolchain.sysrootLibs.resolve("libc++_shared.so"),
+            File("libc++_shared.so")
+        )
+    }
+
+    run {
+        // JsonCpp has other tests, but they require running Python on the
+        // device.
+        shellTest(
+            "jsoncpp_test", listOf(
+                "LD_LIBRARY_PATH=$deviceDirectory",
+                deviceDirectory.resolve("jsoncpp_test").toString()
+            )
+        )
+    }
+}
+
+publishing {
+    publications {
+        create<MavenPublication>("maven") {
+            from(components["prefab"])
+            pom {
+                name.set("JsonCpp")
+                description.set("The ndkports AAR for JsonCpp.")
+                url.set(
+                    "https://android.googlesource.com/platform/tools/ndkports"
+                )
+                licenses {
+                    license {
+                        name.set("The JsonCpp License")
+                        url.set("https://github.com/open-source-parsers/jsoncpp/blob/master/LICENSE")
+                        distribution.set("repo")
+                    }
+                }
+                developers {
+                    developer {
+                        name.set("The Android Open Source Project")
+                    }
+                }
+                scm {
+                    url.set("https://android.googlesource.com/platform/tools/ndkports")
+                    connection.set("scm:git:https://android.googlesource.com/platform/tools/ndkports")
+                }
+            }
+        }
+    }
+
+    repositories {
+        maven {
+            url = uri("${rootProject.buildDir}/repository")
+        }
+    }
+}
diff --git a/jsoncpp/src.tar.gz b/jsoncpp/src.tar.gz
new file mode 100644
index 0000000..35052af
--- /dev/null
+++ b/jsoncpp/src.tar.gz
Binary files differ
diff --git a/openssl/build.gradle.kts b/openssl/build.gradle.kts
new file mode 100644
index 0000000..b7c7a48
--- /dev/null
+++ b/openssl/build.gradle.kts
@@ -0,0 +1,180 @@
+import com.android.ndkports.AdHocPortTask
+import com.android.ndkports.AndroidExecutableTestTask
+import com.android.ndkports.CMakeCompatibleVersion
+
+fun openSslVersionToCMakeVersion(openSslVersion: String): CMakeCompatibleVersion {
+    val (major, minor, microAndLetter) = openSslVersion.split(".")
+    val letter = microAndLetter.last()
+    val micro = microAndLetter.substringBefore(letter)
+    val tweak = if (letter.isDigit()) {
+        // 1.1.1 is 1.1.1.0.
+        0
+    } else {
+        // 1.1.1a is 1.1.1.1.
+        letter.toInt() - 'a'.toInt() + 1
+    }
+
+    return CMakeCompatibleVersion(
+        major.toInt(), minor.toInt(), micro.toInt(), tweak
+    )
+}
+
+val portVersion = "1.1.1l"
+val prefabVersion = openSslVersionToCMakeVersion(portVersion)
+
+group = "com.android.ndk.thirdparty"
+version = "$portVersion${rootProject.extra.get("snapshotSuffix")}"
+
+plugins {
+    id("maven-publish")
+    id("com.android.ndkports.NdkPorts")
+}
+
+ndkPorts {
+    ndkPath.set(File(project.findProperty("ndkPath") as String))
+    source.set(project.file("src.tar.gz"))
+    minSdkVersion.set(16)
+}
+
+val buildTask = tasks.register<AdHocPortTask>("buildPort") {
+    builder {
+        run {
+            args(
+                sourceDirectory.resolve("Configure").absolutePath,
+                "android-${toolchain.abi.archName}",
+                "-D__ANDROID_API__=${toolchain.api}",
+                "--prefix=${installDirectory.absolutePath}",
+                "--openssldir=${installDirectory.absolutePath}",
+                "no-sctp",
+                "shared"
+            )
+
+            env("ANDROID_NDK", toolchain.ndk.path.absolutePath)
+            env("PATH", "${toolchain.binDir}:${System.getenv("PATH")}")
+        }
+
+        run {
+            args("make", "-j$ncpus", "SHLIB_EXT=.so")
+
+            env("ANDROID_NDK", toolchain.ndk.path.absolutePath)
+            env("PATH", "${toolchain.binDir}:${System.getenv("PATH")}")
+        }
+
+        run {
+            args("make", "install_sw", "SHLIB_EXT=.so")
+
+            env("ANDROID_NDK", toolchain.ndk.path.absolutePath)
+            env("PATH", "${toolchain.binDir}:${System.getenv("PATH")}")
+        }
+    }
+}
+
+tasks.prefabPackage {
+    version.set(prefabVersion)
+
+    modules {
+        create("crypto")
+        create("ssl")
+    }
+}
+
+tasks.register<AndroidExecutableTestTask>("test") {
+    val srcDir = tasks.extractSrc.get().outDir.asFile.get()
+    val testSrc = srcDir.resolve("test/ssl-tests")
+    val deviceTestRelPath = File("testconf")
+
+    val unsupportedTests = listOf(
+        // This test is empty and appears to just be broken in 1.1.1k.
+        "16-certstatus.conf",
+        // zlib support is not enabled.
+        "22-compression.conf",
+        // Android does not support SCTP sockets and this test requires them.
+        "29-dtls-sctp-label-bug.conf"
+    )
+
+    push {
+        val ignoredExtensions = listOf("o", "d")
+        val buildDirectory = buildTask.get().buildDirectoryFor(abi)
+        push(
+            srcDir.resolve("test/ct/log_list.conf"), File("log_list.conf")
+        )
+        for (file in buildDirectory.walk()) {
+            if (!file.isFile) {
+                continue
+            }
+
+            if (file.extension in ignoredExtensions) {
+                continue
+            }
+
+            push(file, file.relativeTo(buildDirectory))
+        }
+        for (file in testSrc.walk()) {
+            if (file.extension == "conf") {
+                push(
+                    file, deviceTestRelPath.resolve(file.relativeTo(testSrc))
+                )
+            }
+        }
+        push(srcDir.resolve("test/certs"), File("certs"))
+    }
+
+    run {
+        // https://github.com/openssl/openssl/blob/master/test/README.ssltest.md
+        val sslTest = deviceDirectory.resolve("test/ssl_test")
+        val ctlogFile = deviceDirectory.resolve("log_list.conf")
+        val testCertDir = deviceDirectory.resolve("certs")
+        for (file in testSrc.walk()) {
+            val test = deviceDirectory.resolve(deviceTestRelPath)
+                .resolve(file.relativeTo(testSrc))
+            if (file.extension == "conf" && file.name !in unsupportedTests) {
+                shellTest(
+                    file.relativeTo(testSrc).toString(), listOf(
+                        "LD_LIBRARY_PATH=$deviceDirectory",
+                        "CTLOG_FILE=$ctlogFile",
+                        "TEST_CERTS_DIR=$testCertDir",
+                        sslTest.toString(),
+                        test.toString()
+                    )
+                )
+            }
+        }
+    }
+}
+
+publishing {
+    publications {
+        create<MavenPublication>("maven") {
+            from(components["prefab"])
+            pom {
+                name.set("OpenSSL")
+                description.set("The ndkports AAR for OpenSSL.")
+                url.set(
+                    "https://android.googlesource.com/platform/tools/ndkports"
+                )
+                licenses {
+                    license {
+                        name.set("Dual OpenSSL and SSLeay License")
+                        url.set("https://www.openssl.org/source/license-openssl-ssleay.txt")
+                        distribution.set("repo")
+                    }
+                }
+                developers {
+                    developer {
+                        name.set("The Android Open Source Project")
+                    }
+                }
+                scm {
+                    url.set("https://android.googlesource.com/platform/tools/ndkports")
+                    connection.set("scm:git:https://android.googlesource.com/platform/tools/ndkports")
+                }
+            }
+        }
+    }
+
+    repositories {
+        maven {
+            url = uri("${rootProject.buildDir}/repository")
+        }
+    }
+}
diff --git a/openssl/src.tar.gz b/openssl/src.tar.gz
new file mode 100644
index 0000000..81be7f9
--- /dev/null
+++ b/openssl/src.tar.gz
Binary files differ
diff --git a/ports/curl/port.kts b/ports/curl/port.kts
deleted file mode 100644
index a870a86..0000000
--- a/ports/curl/port.kts
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.ndkports
-
-import java.io.File
-
-object : AutoconfPort() {
-    override val name = "curl"
-    override val version = "7.69.1"
-    override val mavenVersion = "$version-alpha-1"
-    override val licensePath = "COPYING"
-
-    override val license = License(
-        "The curl License", "https://curl.haxx.se/docs/copyright.html"
-    )
-
-    override val dependencies = listOf("openssl")
-
-    override val modules = listOf(
-        Module(
-            "curl",
-            dependencies = listOf("//openssl:crypto", "//openssl:ssl")
-        )
-    )
-
-    override fun configureArgs(
-        workingDirectory: File,
-        toolchain: Toolchain
-    ): List<String> {
-        val sslPrefix = installDirectoryForPort(
-            "openssl",
-            workingDirectory,
-            toolchain
-        ).absolutePath
-        return listOf(
-            "--disable-ntlm-wb",
-            "--enable-ipv6",
-            "--with-zlib",
-            "--with-ca-path=/system/etc/security/cacerts",
-            "--with-ssl=$sslPrefix"
-        )
-    }
-
-    override fun configureEnv(
-        workingDirectory: File,
-        toolchain: Toolchain
-    ): Map<String, String> = mapOf(
-        // aarch64 still defaults to bfd which transitively checks libraries.
-        // When curl is linking one of its own libraries which depends on
-        // openssl, it doesn't pass -rpath-link to be able to find the SSL
-        // libraries and fails to build because of it.
-        //
-        // TODO: Switch to lld once we're using r21.
-        "LDFLAGS" to "-fuse-ld=gold"
-    )
-}
diff --git a/ports/curl/src.tar.gz b/ports/curl/src.tar.gz
deleted file mode 100644
index b0da690..0000000
--- a/ports/curl/src.tar.gz
+++ /dev/null
Binary files differ
diff --git a/ports/jsoncpp/port.kts b/ports/jsoncpp/port.kts
deleted file mode 100644
index 8a3e1ba..0000000
--- a/ports/jsoncpp/port.kts
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.ndkports
-
-import java.io.File
-
-object : MesonPort() {
-    override val name = "jsoncpp"
-    override val version = "1.8.4"
-    override val mavenVersion = "$version-alpha-1"
-
-    override val license = License(
-        "The JsonCpp License",
-        "https://github.com/open-source-parsers/jsoncpp/blob/master/LICENSE"
-    )
-
-    override val modules = listOf(
-        Module("jsoncpp")
-    )
-
-    override fun extractSource(
-        sourceTarball: File,
-        sourceDirectory: File,
-        workingDirectory: File
-    ): Result<Unit, String> =
-        super.extractSource(sourceTarball, sourceDirectory, workingDirectory)
-            .onSuccess {
-                // jsoncpp has a "version" file on the include path that
-                // conflicts with
-                // https://en.cppreference.com/w/cpp/header/version. Remove it
-                // so we can build.
-                sourceDirectory.resolve("version").delete()
-            }
-}
diff --git a/ports/jsoncpp/src.tar.gz b/ports/jsoncpp/src.tar.gz
deleted file mode 100644
index 7f3cd4b..0000000
--- a/ports/jsoncpp/src.tar.gz
+++ /dev/null
Binary files differ
diff --git a/ports/openssl/port.kts b/ports/openssl/port.kts
deleted file mode 100644
index 45b5206..0000000
--- a/ports/openssl/port.kts
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.ndkports
-
-import java.io.File
-
-object : Port() {
-    override val name = "openssl"
-    override val version = "1.1.1g"
-    override val mavenVersion = "$version-alpha-1"
-    override val prefabVersion = CMakeCompatibleVersion(1, 1, 1, 7)
-
-    override val license = License(
-        "Dual OpenSSL and SSLeay License",
-        "https://www.openssl.org/source/license-openssl-ssleay.txt"
-    )
-
-    override val modules = listOf(
-        Module("crypto"),
-        Module("ssl")
-    )
-
-    override fun configure(
-        toolchain: Toolchain,
-        sourceDirectory: File,
-        buildDirectory: File,
-        installDirectory: File,
-        workingDirectory: File
-    ): Result<Unit, String> {
-        buildDirectory.mkdirs()
-        return executeProcessStep(
-            listOf(
-                sourceDirectory.resolve("Configure").absolutePath,
-                "android-${toolchain.abi.archName}",
-                "-D__ANDROID_API__=${toolchain.api}",
-                "--prefix=${installDirectory.absolutePath}",
-                "--openssldir=${installDirectory.absolutePath}",
-                "shared"
-            ),
-            buildDirectory,
-            additionalEnvironment = mapOf(
-                "ANDROID_NDK" to toolchain.ndk.path.absolutePath,
-                "PATH" to "${toolchain.binDir}:${System.getenv("PATH")}"
-            )
-        )
-    }
-
-    override fun build(
-        toolchain: Toolchain,
-        buildDirectory: File
-    ): Result<Unit, String> =
-        executeProcessStep(
-            listOf(
-                "make",
-                "-j$ncpus",
-                "SHLIB_EXT=.so"
-            ), buildDirectory,
-            additionalEnvironment = mapOf(
-                "ANDROID_NDK" to toolchain.ndk.path.absolutePath,
-                "PATH" to "${toolchain.binDir}:${System.getenv("PATH")}"
-            )
-        )
-
-    override fun install(
-        toolchain: Toolchain,
-        buildDirectory: File,
-        installDirectory: File
-    ): Result<Unit, String> =
-        executeProcessStep(
-            listOf("make", "install_sw", "SHLIB_EXT=.so"), buildDirectory,
-            additionalEnvironment = mapOf(
-                "ANDROID_NDK" to toolchain.ndk.path.absolutePath,
-                "PATH" to "${toolchain.binDir}:${System.getenv("PATH")}"
-            )
-        )
-}
diff --git a/ports/openssl/src.tar.gz b/ports/openssl/src.tar.gz
deleted file mode 100644
index e768f9e..0000000
--- a/ports/openssl/src.tar.gz
+++ /dev/null
Binary files differ
diff --git a/scripts/build.sh b/scripts/build.sh
deleted file mode 100755
index e7c16b6..0000000
--- a/scripts/build.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/bash
-set -e
-docker build -t ndkports .
-docker run --rm -v $(pwd):/src ndkports
diff --git a/scripts/build_release.sh b/scripts/build_release.sh
new file mode 100755
index 0000000..f99e223
--- /dev/null
+++ b/scripts/build_release.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+set -e
+docker build -t ndkports .
+# We need to specify the full argument list for gradle explicitly because
+# there's no way to append to docker's CMD. This should be kept the same as the
+# default, but adding -Prelease.
+docker run --rm -v $(pwd):/src ndkports \
+  --stacktrace -PndkPath=/ndk -Prelease release
diff --git a/scripts/build_snapshot.sh b/scripts/build_snapshot.sh
new file mode 100755
index 0000000..4c9d767
--- /dev/null
+++ b/scripts/build_snapshot.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -e
+docker build -t ndkports .
+# Default command for the docker image handles the NDK location, --stacktrace,
+# task list, etc.
+docker run --rm -v $(pwd):/src ndkports
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 4549d2e..726326c 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -8,4 +8,9 @@
             }
         }
     }
-}
\ No newline at end of file
+}
+
+include("curl")
+include("googletest")
+include("jsoncpp")
+include("openssl")
diff --git a/src/main/kotlin/com/android/ndkports/Abi.kt b/src/main/kotlin/com/android/ndkports/Abi.kt
deleted file mode 100644
index 58981a2..0000000
--- a/src/main/kotlin/com/android/ndkports/Abi.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.ndkports
-
-enum class Abi(val archName: String, val abiName: String) {
-    Arm("arm", "armeabi-v7a"),
-    Arm64("arm64", "arm64-v8a"),
-    X86("x86", "x86"),
-    X86_64("x86_64", "x86_64"),
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/android/ndkports/AutoconfPort.kt b/src/main/kotlin/com/android/ndkports/AutoconfPort.kt
deleted file mode 100644
index 1c009cb..0000000
--- a/src/main/kotlin/com/android/ndkports/AutoconfPort.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.ndkports
-
-import java.io.File
-
-abstract class AutoconfPort : Port() {
-    open fun configureArgs(
-        workingDirectory: File,
-        toolchain: Toolchain
-    ): List<String> = emptyList()
-
-    open fun configureEnv(
-        workingDirectory: File,
-        toolchain: Toolchain
-    ): Map<String, String> = emptyMap()
-
-    override fun configure(
-        toolchain: Toolchain,
-        sourceDirectory: File,
-        buildDirectory: File,
-        installDirectory: File,
-        workingDirectory: File
-    ): Result<Unit, String> {
-        buildDirectory.mkdirs()
-        return executeProcessStep(
-            listOf(
-                "${sourceDirectory.absolutePath}/configure",
-                "--host=${toolchain.binutilsTriple}",
-                "--prefix=${installDirectory.absolutePath}"
-            ) + configureArgs(workingDirectory, toolchain),
-            buildDirectory,
-            additionalEnvironment = mutableMapOf(
-                "AR" to toolchain.ar.absolutePath,
-                "CC" to toolchain.clang.absolutePath,
-                "CXX" to toolchain.clangxx.absolutePath,
-                "RANLIB" to toolchain.ranlib.absolutePath,
-                "STRIP" to toolchain.strip.absolutePath,
-                "PATH" to "${toolchain.binDir}:${System.getenv("PATH")}"
-            ).apply { putAll(configureEnv(workingDirectory, toolchain)) }
-        )
-    }
-
-    override fun build(
-        toolchain: Toolchain,
-        buildDirectory: File
-    ): Result<Unit, String> =
-        executeProcessStep(
-            listOf("make", "-j$ncpus"), buildDirectory
-        )
-
-    override fun install(
-        toolchain: Toolchain,
-        buildDirectory: File,
-        installDirectory: File
-    ): Result<Unit, String> =
-        executeProcessStep(
-            listOf("make", "-j$ncpus", "install"), buildDirectory
-        )
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/android/ndkports/Cli.kt b/src/main/kotlin/com/android/ndkports/Cli.kt
deleted file mode 100644
index e2a5879..0000000
--- a/src/main/kotlin/com/android/ndkports/Cli.kt
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.ndkports
-
-import com.github.ajalt.clikt.core.CliktCommand
-import com.github.ajalt.clikt.parameters.arguments.argument
-import com.github.ajalt.clikt.parameters.arguments.multiple
-import com.github.ajalt.clikt.parameters.arguments.validate
-import com.github.ajalt.clikt.parameters.options.convert
-import com.github.ajalt.clikt.parameters.options.default
-import com.github.ajalt.clikt.parameters.options.flag
-import com.github.ajalt.clikt.parameters.options.option
-import com.github.ajalt.clikt.parameters.options.required
-import com.github.ajalt.clikt.parameters.types.file
-import de.swirtz.ktsrunner.objectloader.KtsObjectLoader
-import java.io.File
-import java.io.FileNotFoundException
-import java.lang.RuntimeException
-import kotlin.system.exitProcess
-
-class Cli : CliktCommand(help = "ndkports") {
-    private val outDir: File by option(
-        "-o",
-        "--out",
-        help = "Build directory."
-    ).file().default(File("out"))
-
-    private val publishToMavenLocal: Boolean by option(
-        help = "Publish AARs to the local Maven repository (~/.m2/repository)"
-    ).flag()
-
-    private val packages: List<String> by argument(
-        help = "Names of packages to build."
-    ).multiple().validate {
-        require(it.isNotEmpty()) { "must provide at least one package" }
-    }
-
-    private val ndk: Ndk by option().convert { Ndk(File(it)) }.required()
-
-    private fun portDirectoryFor(name: String): File =
-        File("ports").resolve(name)
-
-    private fun loadPort(name: String): Port {
-        val portDir = portDirectoryFor(name).also {
-            if (!it.exists()) {
-                throw FileNotFoundException("Could not find ${it.path}")
-            }
-        }
-
-        val portFile = portDir.resolve("port.kts").also {
-            if (!it.exists()) {
-                throw FileNotFoundException("Could not find ${it.path}")
-            }
-        }
-
-        return KtsObjectLoader().load(portFile.reader())
-    }
-
-    override fun run() {
-        println("Building packages: ${packages.joinToString(", ")}")
-        val portsByName = packages.map { loadPort(it) }.associateBy { it.name }
-        for (port in portsByName.values) {
-            val workingDirectory =
-                outDir.resolve(port.name).also { it.mkdirs() }
-
-            val sourceDirectory = workingDirectory.resolve("src")
-            val sourceTarball =
-                portDirectoryFor(port.name).resolve("src.tar.gz")
-
-            port.extractSource(sourceTarball, sourceDirectory, workingDirectory)
-
-            val apiForAbi = mapOf(
-                Abi.Arm to 16,
-                Abi.Arm64 to 21,
-                Abi.X86 to 16,
-                Abi.X86_64 to 21
-            )
-            for (abi in Abi.values()) {
-                val api = apiForAbi.getOrElse(abi) {
-                    throw RuntimeException(
-                        "No API level specified for ${abi.abiName}"
-                    )
-                }
-                val toolchain = Toolchain(ndk, abi, api)
-
-                val buildDirectory = workingDirectory.resolve("build/$abi")
-                val installDirectory = installDirectoryForPort(
-                    port.name, workingDirectory, toolchain
-                )
-
-                port.run(
-                    toolchain,
-                    sourceDirectory,
-                    buildDirectory,
-                    installDirectory,
-                    workingDirectory
-                ).onFailure {
-                    println(it)
-                    exitProcess(1)
-                }
-            }
-
-            PrefabPackageBuilder(
-                port,
-                workingDirectory,
-                sourceDirectory,
-                publishToMavenLocal,
-                ndk,
-                apiForAbi,
-                portsByName
-            ).build()
-        }
-    }
-}
-
-fun main(args: Array<String>) {
-    Cli().main(args)
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/android/ndkports/License.kt b/src/main/kotlin/com/android/ndkports/License.kt
deleted file mode 100644
index 8ec639f..0000000
--- a/src/main/kotlin/com/android/ndkports/License.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.ndkports
-
-data class License(val name: String, val url: String)
\ No newline at end of file
diff --git a/src/main/kotlin/com/android/ndkports/Port.kt b/src/main/kotlin/com/android/ndkports/Port.kt
deleted file mode 100644
index 2e0433f..0000000
--- a/src/main/kotlin/com/android/ndkports/Port.kt
+++ /dev/null
@@ -1,284 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.ndkports
-
-import java.io.File
-
-@Suppress("unused")
-fun executeProcessStep(
-    args: List<String>,
-    workingDirectory: File,
-    additionalEnvironment: Map<String, String>? = null
-): Result<Unit, String> {
-    val pb = ProcessBuilder(args)
-        .redirectOutput(ProcessBuilder.Redirect.INHERIT)
-        .redirectError(ProcessBuilder.Redirect.INHERIT)
-        .directory(workingDirectory)
-
-    if (additionalEnvironment != null) {
-        pb.environment().putAll(additionalEnvironment)
-    }
-
-    return pb.start()
-        .waitFor().let {
-            if (it == 0) {
-                Result.Ok(Unit)
-            } else {
-                Result.Error("Process failed with exit code $it")
-            }
-        }
-}
-
-fun installDirectoryForPort(
-    name: String,
-    workingDirectory: File,
-    toolchain: Toolchain
-): File = workingDirectory.parentFile.resolve("$name/install/${toolchain.abi}")
-
-/**
- * A module exported by the package.
- *
- * As currently implemented by ndkports, one module is exactly one library.
- * Prefab supports header-only libraries, but ndkports does not support these
- * yet.
- *
- * Static libraries are not currently supported by ndkports.
- *
- * @property[name] The name of the module. Note that currently the name of the
- * installed library file must be exactly `lib$name.so`.
- * @property[includesPerAbi] Set to true if a different set of headers should be
- * exposed per-ABI. Not currently implemented.
- * @property[dependencies] A list of other modules required by this module, in
- * the format described by https://google.github.io/prefab/.
- */
-data class Module(
-    val name: String,
-    val includesPerAbi: Boolean = false,
-    val dependencies: List<String> = emptyList()
-)
-
-/**
- * The base class for all ports.
- */
-abstract class Port {
-    /**
-     * The name of the port. Will be used as the package name in prefab.json.
-     */
-    abstract val name: String
-
-    /**
-     * The version of the package.
-     *
-     * Used as the default for [prefabVersion] and [mavenVersion] when
-     * appropriate.
-     */
-    abstract val version: String
-
-    /**
-     * The version to encode in the prefab.json.
-     *
-     * This version must be compatible with CMake's `find_package` for
-     * config-style packages. This means that it must be one to four decimal
-     * separated integers. No other format is allowed.
-     *
-     * If not set, the default is [version] as interpreted by
-     * [CMakeCompatibleVersion.parse].
-     *
-     * For example, OpenSSL 1.1.1g will set this value to
-     * `CMakeCompatibleVersion(1, 1, 1, 7)`.
-     */
-    open val prefabVersion: CMakeCompatibleVersion
-        get() = CMakeCompatibleVersion.parse(version)
-
-    /**
-     * The version to use for the maven package.
-     *
-     * This field allows versioning the maven package differently from the
-     * package itself, which is sometimes necessary given CMake's strict version
-     * format requirements.
-     *
-     * If not set, the default is [version].
-     *
-     * For example, this could be set to `"$name-$version-alpha-1"` to
-     * distribute an alpha of the package.
-     */
-    open val mavenVersion: String
-        get() = version
-
-    /**
-     * The relative path to the license file of this package.
-     *
-     * This file will be packaged in the AAR in the META-INF directory.
-     */
-    open val licensePath: String = "LICENSE"
-
-    /**
-     * A description of the license (name and URL) for use in the pom.xml.
-     */
-    abstract val license: License
-
-    /**
-     * A list of dependencies for this package.
-     *
-     * For example, curl depends on `listOf("openssl")`.
-     */
-    open val dependencies: List<String> = emptyList()
-
-    /**
-     * A list of modules exported by this package.
-     */
-    abstract val modules: List<Module>
-
-    /**
-     * The number of CPUs available for building.
-     *
-     * May be passed to the build system if required.
-     */
-    protected val ncpus = Runtime.getRuntime().availableProcessors()
-
-    fun run(
-        toolchain: Toolchain,
-        sourceDirectory: File,
-        buildDirectory: File,
-        installDirectory: File,
-        workingDirectory: File
-    ): Result<Unit, String> {
-        configure(
-            toolchain,
-            sourceDirectory,
-            buildDirectory,
-            installDirectory,
-            workingDirectory
-        ).onFailure { return Result.Error(it) }
-
-        build(toolchain, buildDirectory).onFailure { return Result.Error(it) }
-
-        install(
-            toolchain,
-            buildDirectory,
-            installDirectory
-        ).onFailure { return Result.Error(it) }
-
-        return Result.Ok(Unit)
-    }
-
-    /**
-     * Overridable build step for extracting the source package.
-     *
-     * @param[sourceTarball] The path to the source tarball.
-     * @param[sourceDirectory] The destination directory for the extracted
-     * source.
-     * @param[workingDirectory] The working top-level directory for this
-     * package.
-     * @return A [Result<Unit, String>][Result] describing the result of the
-     * operation. On failure, [Result.Error<String>][Result.Error] containing an
-     * error message is returned.
-     */
-    open fun extractSource(
-        sourceTarball: File,
-        sourceDirectory: File,
-        workingDirectory: File
-    ): Result<Unit, String> {
-        sourceDirectory.mkdirs()
-        return executeProcessStep(
-            listOf(
-                "tar",
-                "xf",
-                sourceTarball.absolutePath,
-                "--strip-components=1"
-            ), sourceDirectory
-        )
-    }
-
-    /**
-     * Overridable build step for configuring the build.
-     *
-     * Any pre-build steps should be run here.
-     *
-     * In an autoconf build, this runs `configure`.
-     *
-     * @param[toolchain] The toolchain used for this build.
-     * @param[sourceDirectory] The directory containing the extracted package
-     * source.
-     * @param[buildDirectory] The directory to use for building.
-     * @param[installDirectory] The destination directory for this package's
-     * installed headers and libraries.
-     * @param[workingDirectory] The top-level working directory for this
-     * package.
-     * @return A [Result<Unit, String>][Result] describing the result of the
-     * operation. On failure, [Result.Error<String>][Result.Error] containing an
-     * error message is returned.
-     */
-    open fun configure(
-        toolchain: Toolchain,
-        sourceDirectory: File,
-        buildDirectory: File,
-        installDirectory: File,
-        workingDirectory: File
-    ): Result<Unit, String> = Result.Ok(Unit)
-
-    /**
-     * Overridable build step for building the package.
-     *
-     * In an autoconf build, this runs `make`.
-     *
-     * @param[toolchain] The toolchain used for this build.
-     * @param[buildDirectory] The directory to use for building.
-     * @return A [Result<Unit, String>][Result] describing the result of the
-     * operation. On failure, [Result.Error<String>][Result.Error] containing an
-     * error message is returned.
-     */
-    open fun build(
-        toolchain: Toolchain,
-        buildDirectory: File
-    ): Result<Unit, String> = Result.Ok(Unit)
-
-    /**
-     * Overridable build step for installing built artifacts for packaging.
-     *
-     * The install step is expected to install headers and libraries to the
-     * [installDirectory] with the following layout:
-     *
-     * [installDirectory]
-     *     include/
-     *         <package headers>
-     *     lib/
-     *         <package libraries>
-     *
-     * A matching `lib${module.name}.so` must be present in the `lib` directory
-     * for every item in [modules].
-     *
-     * Note that it is expected for all modules to use the same headers. This is
-     * currently the case for all ports currently maintained, but could change
-     * in the future.
-     *
-     * In an autoconf build, this runs `make install`.
-     *
-     * @param[toolchain] The toolchain used for this build.
-     * @param[buildDirectory] The directory containing build artifacts.
-     * @param[installDirectory] The destination directory for this package's
-     * installed headers and libraries.
-     * @return A [Result<Unit, String>][Result] describing the result of the
-     * operation. On failure, [Result.Error<String>][Result.Error] containing an
-     * error message is returned.
-     */
-    open fun install(
-        toolchain: Toolchain,
-        buildDirectory: File,
-        installDirectory: File
-    ): Result<Unit, String> = Result.Ok(Unit)
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/android/ndkports/PrefabPackageBuilder.kt b/src/main/kotlin/com/android/ndkports/PrefabPackageBuilder.kt
deleted file mode 100644
index 1a58f5a..0000000
--- a/src/main/kotlin/com/android/ndkports/PrefabPackageBuilder.kt
+++ /dev/null
@@ -1,253 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.ndkports
-
-import com.google.prefab.api.AndroidAbiMetadata
-import com.google.prefab.api.ModuleMetadataV1
-import com.google.prefab.api.PackageMetadataV1
-import kotlinx.serialization.json.Json
-import kotlinx.serialization.stringify
-import org.apache.maven.model.Dependency
-import org.apache.maven.model.Developer
-import org.apache.maven.model.License
-import org.apache.maven.model.Scm
-import org.apache.maven.model.io.DefaultModelWriter
-import org.apache.maven.project.MavenProject
-import org.redundent.kotlin.xml.xml
-import java.io.File
-
-class PrefabPackageBuilder(
-    private val port: Port,
-    private val directory: File,
-    private val sourceDirectory: File,
-    private val publishToMavenLocal: Boolean,
-    private val ndk: Ndk,
-    private val abiToApiMap: Map<Abi, Int>,
-    private val portsByName: Map<String, Port>
-) {
-    private val packageDirectory = directory.resolve("aar")
-    private val prefabDirectory = packageDirectory.resolve("prefab")
-    private val modulesDirectory = prefabDirectory.resolve("modules")
-
-    private val packageComponents = listOf(
-        "com",
-        "android",
-        "ndk",
-        "thirdparty",
-        port.name
-    )
-
-    private val packageName = packageComponents.joinToString(".")
-    private val groupComponents = packageComponents.dropLast(1)
-    private val groupId = groupComponents.joinToString(".")
-    private val artifactId = packageComponents.last()
-
-    private val mavenProject = MavenProject().also {
-        it.name = port.name
-        it.description = "The ndkports AAR for ${port.name}."
-        it.url = "https://android.googlesource.com/platform/tools/ndkports"
-        it.groupId = groupId
-        it.artifactId = artifactId
-        it.version = port.mavenVersion
-        it.packaging = "aar"
-        it.licenses = listOf(
-            License().also { license ->
-                license.name = port.license.name
-                license.url = port.license.url
-                license.distribution = "repo"
-            }
-        )
-        it.developers = listOf(
-            Developer().also { developer ->
-                developer.name = "The Android Open Source Project"
-            }
-        )
-        it.scm = Scm().also { scm ->
-            scm.url = "https://android.googlesource.com/platform/tools/ndkports"
-            scm.connection = "scm:git:https://android.googlesource.com/platform/tools/ndkports"
-        }
-        it.dependencies = port.dependencies.map { depName ->
-            val depPort = portsByName[depName] ?: throw RuntimeException(
-                "${port.name} depends on unknown port: $depName"
-            )
-            Dependency().also { dep ->
-                dep.artifactId = depPort.name
-                dep.groupId = groupId
-                dep.version = depPort.mavenVersion
-                dep.type = "aar"
-                // TODO: Make this an option in the Port.
-                // We currently only have one dependency from curl to OpenSSL,
-                // and that's (from the perspective of the AAR consumer), a
-                // runtime dependency. If we ever have compile dependencies,
-                // we'll want to make it possible for each port to customize its
-                // scope.
-                dep.scope = "runtime"
-            }
-        }
-        // TODO: Issue management?
-    }
-
-    private fun preparePackageDirectory() {
-        if (packageDirectory.exists()) {
-            packageDirectory.deleteRecursively()
-        }
-        modulesDirectory.mkdirs()
-    }
-
-    private fun makePackageMetadata() {
-        prefabDirectory.resolve("prefab.json").writeText(
-            Json.stringify(
-                PackageMetadataV1(
-                    port.name,
-                    schemaVersion = 1,
-                    dependencies = port.dependencies,
-                    version = port.prefabVersion.toString()
-                )
-            )
-        )
-    }
-
-    private fun makeModuleMetadata(module: Module, moduleDirectory: File) {
-        moduleDirectory.resolve("module.json").writeText(
-            Json.stringify(
-                ModuleMetadataV1(
-                    exportLibraries = module.dependencies
-                )
-            )
-        )
-    }
-
-    private fun installLibForAbi(module: Module, abi: Abi, libsDir: File) {
-        val libName = "lib${module.name}.so"
-        val installDirectory = libsDir.resolve("android.${abi.abiName}").apply {
-            mkdirs()
-        }
-
-        directory.resolve("install/$abi/lib/$libName")
-            .copyTo(installDirectory.resolve(libName))
-
-        val api = abiToApiMap.getOrElse(abi) {
-            throw RuntimeException(
-                "No API level specified for ${abi.abiName}"
-            )
-        }
-
-        installDirectory.resolve("abi.json").writeText(
-            Json.stringify(
-                AndroidAbiMetadata(
-                    abi = abi.abiName,
-                    api = api,
-                    ndk = ndk.version.major,
-                    stl = "c++_shared"
-                )
-            )
-        )
-    }
-
-    private fun installLicense() {
-        val src = sourceDirectory.resolve(port.licensePath)
-        val dest = packageDirectory.resolve("META-INF")
-            .resolve(File(port.licensePath).name)
-        src.copyTo(dest)
-    }
-
-    private fun createAndroidManifest() {
-        packageDirectory.resolve("AndroidManifest.xml")
-            .writeText(xml("manifest") {
-                attributes(
-                    "xmlns:android" to "http://schemas.android.com/apk/res/android",
-                    "package" to packageName,
-                    "android:versionCode" to 1,
-                    "android:versionName" to "1.0"
-                )
-
-                "uses-sdk" {
-                    attributes(
-                        "android:minSdkVersion" to 16,
-                        "android:targetSdkVersion" to 29
-                    )
-                }
-            }.toString())
-    }
-
-    private fun createPom(pomFile: File) {
-        DefaultModelWriter().write(pomFile, null, mavenProject.model)
-    }
-
-    private fun installToLocalMaven(archive: File, pomFile: File) {
-        val pb = ProcessBuilder(
-            listOf(
-                "mvn",
-                "install:install-file",
-                "-Dfile=$archive",
-                "-DpomFile=$pomFile"
-            )
-        )
-            .redirectOutput(ProcessBuilder.Redirect.INHERIT)
-            .redirectError(ProcessBuilder.Redirect.INHERIT)
-
-        return pb.start()
-            .waitFor().let {
-                if (it != 0) {
-                    throw RuntimeException(
-                        "Failed to install archive to local maven " +
-                                "repository: $archive $pomFile"
-                    )
-                }
-            }
-    }
-
-    private fun createArchive() {
-        val archive = directory.resolve("${port.name}-${port.mavenVersion}.aar")
-        val pomFile = directory.resolve("${port.name}-${port.mavenVersion}.pom")
-        createZipFromDirectory(archive, packageDirectory)
-        createPom(pomFile)
-        if (publishToMavenLocal) {
-            installToLocalMaven(archive, pomFile)
-        }
-    }
-
-    fun build() {
-        preparePackageDirectory()
-        makePackageMetadata()
-        for (module in port.modules) {
-            val moduleDirectory = modulesDirectory.resolve(module.name).apply {
-                mkdirs()
-            }
-
-            makeModuleMetadata(module, moduleDirectory)
-
-            if (module.includesPerAbi) {
-                TODO()
-            } else {
-                // TODO: Perform sanity check.
-                directory.resolve("install/${Abi.Arm}/include")
-                    .copyRecursively(moduleDirectory.resolve("include"))
-            }
-
-            val libsDir = moduleDirectory.resolve("libs").apply { mkdirs() }
-            for (abi in Abi.values()) {
-                installLibForAbi(module, abi, libsDir)
-            }
-        }
-
-        installLicense()
-
-        createAndroidManifest()
-        createArchive()
-    }
-}
diff --git a/src/main/kotlin/com/android/ndkports/Result.kt b/src/main/kotlin/com/android/ndkports/Result.kt
deleted file mode 100644
index 21c203e..0000000
--- a/src/main/kotlin/com/android/ndkports/Result.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.ndkports
-
-sealed class Result<out T, out E> {
-    data class Ok<T>(val value: T) : Result<T, Nothing>()
-    data class Error<E>(val error: E) : Result<Nothing, E>()
-
-    inline fun onSuccess(block: (T) -> Unit): Result<T, E> {
-        if (this is Ok<T>) {
-            block(value)
-        }
-        return this
-    }
-
-    inline fun onFailure(block: (E) -> Unit): Result<T, E> {
-        if (this is Error<E>) {
-            block(error)
-        }
-        return this
-    }
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/android/ndkports/Zip.kt b/src/main/kotlin/com/android/ndkports/Zip.kt
deleted file mode 100644
index a23ff90..0000000
--- a/src/main/kotlin/com/android/ndkports/Zip.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.ndkports
-
-import java.io.File
-import java.io.FileInputStream
-import java.io.FileOutputStream
-import java.util.zip.ZipEntry
-import java.util.zip.ZipOutputStream
-
-private fun zipDirectory(name: String, zipOut: ZipOutputStream) {
-    zipOut.putNextEntry(ZipEntry("$name/"))
-    zipOut.closeEntry()
-}
-
-private fun zipFile(file: File, name: String, zipOut: ZipOutputStream) {
-    zipOut.putNextEntry(ZipEntry(name))
-    FileInputStream(file).use {
-        it.copyTo(zipOut)
-    }
-}
-
-private fun zip(file: File, name: String, zipOut: ZipOutputStream) {
-    if (file.isDirectory) {
-        zipDirectory(name, zipOut)
-    } else {
-        zipFile(file, name, zipOut)
-    }
-}
-
-fun createZipFromDirectory(output: File, input: File) {
-    FileOutputStream(output).use { fos ->
-        ZipOutputStream(fos).use { zos ->
-            input.walk().filter { it != input }.forEach {
-                zip(it, it.relativeTo(input).path, zos)
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/test/kotlin/com/android/ndkports/ResultTest.kt b/src/test/kotlin/com/android/ndkports/ResultTest.kt
deleted file mode 100644
index 0abef18..0000000
--- a/src/test/kotlin/com/android/ndkports/ResultTest.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.ndkports
-
-import kotlin.test.Test
-import kotlin.test.assertEquals
-import kotlin.test.fail
-
-class ResultTest {
-    @Test
-    fun `onFailure executes block on failure`() {
-        val result = Result.Error("foo")
-        result.onFailure {
-            assertEquals("foo", it)
-            return
-        }
-        fail()
-    }
-
-    @Test
-    fun `onFailure does not execute block on success`() {
-        val result = Result.Ok(Unit)
-        result.onFailure { fail() }
-    }
-
-    @Test
-    fun `onFailure returns same result object`() {
-        val result = Result.Error(Unit)
-        assertEquals(result, result.onFailure {})
-    }
-
-    @Test
-    fun `onSuccess executes block on success`() {
-        val result = Result.Ok("foo")
-        result.onSuccess {
-            assertEquals("foo", it)
-            return
-        }
-        fail()
-    }
-
-    @Test
-    fun `onSuccess does not execute block on failure`() {
-        val result = Result.Error(Unit)
-        result.onSuccess { fail() }
-    }
-
-    @Test
-    fun `onSuccess returns same result object`() {
-        val result = Result.Ok(Unit)
-        assertEquals(result, result.onSuccess {})
-    }
-}
\ No newline at end of file