Snap for 6073191 from f7f779d2132c7c0e50bd90b00b860489ff1b50bb to rvc-release

Change-Id: I07b1a8d1cfab2658c4166a54bce423517ff9cdcc
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..14e1cbc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,83 @@
+.gradle
+/build/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+# Cache of project
+.gradletasknamecache
+
+# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
+# gradle/wrapper/gradle-wrapper.properties
+
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# 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
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..e3439a9
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,16 @@
+<component name="ProjectCodeStyleConfiguration">
+  <code_scheme name="Project" version="173">
+    <option name="RIGHT_MARGIN" value="80" />
+    <option name="WRAP_WHEN_TYPING_REACHES_RIGHT_MARGIN" value="true" />
+    <option name="SOFT_MARGINS" value="80" />
+    <JetCodeStyleSettings>
+      <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
+      <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
+      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+    </JetCodeStyleSettings>
+    <codeStyleSettings language="kotlin">
+      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+      <option name="WRAP_ON_TYPING" value="0" />
+    </codeStyleSettings>
+  </code_scheme>
+</component>
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+<component name="ProjectCodeStyleConfiguration">
+  <state>
+    <option name="USE_PER_PROJECT_SETTINGS" value="true" />
+  </state>
+</component>
\ No newline at end of file
diff --git a/.idea/kotlinCodeInsightSettings.xml b/.idea/kotlinCodeInsightSettings.xml
new file mode 100644
index 0000000..71e404d
--- /dev/null
+++ b/.idea/kotlinCodeInsightSettings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="KotlinCodeInsightWorkspaceSettings">
+    <option name="addUnambiguousImportsOnTheFly" value="true" />
+    <option name="optimizeImportsOnTheFly" value="true" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/kotlinScripting.xml b/.idea/kotlinScripting.xml
new file mode 100644
index 0000000..a6fe551
--- /dev/null
+++ b/.idea/kotlinScripting.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="KotlinScriptingSettings">
+    <option name="isAutoReloadEnabled" value="true" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..bc8d0a3
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +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">
+    <output url="file://$PROJECT_DIR$/out" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..347b2d7
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+<?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
new file mode 100644
index 0000000..d6ebd48
--- /dev/null
+++ b/.idea/ndkports.iml
@@ -0,0 +1,9 @@
+<?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
new file mode 100644
index 0000000..73193ea
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="IssueNavigationConfiguration">
+    <option name="links">
+      <list>
+        <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>
+      </list>
+    </option>
+  </component>
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a40c3df
--- /dev/null
+++ b/README.md
@@ -0,0 +1,74 @@
+# ndkports
+
+A collection of Android build scripts for various third-party libraries and the
+tooling to build them.
+
+If you're an Android app developer looking to *consume* these libraries, this is
+probably not what you want. This project builds AARs to be published to Maven.
+You most likely want to use the AAR, not build it yourself.
+
+Note: Gradle support for consuming these artifacts from an AAR is a work in
+progress.
+
+## Ports
+
+Each third-party project is called a "port". Ports consist of a description of
+where to fetch the source, apply any patches needed, build, install, and package
+the library into an AAR.
+
+A port is a subclass of the abstract Kotlin class `com.android.ndkports.Port`.
+Projects define the name and version of the port, the URL to fetch source from,
+a list of modules (libraries) to build, and the build steps.
+
+```kotlin
+abstract class Port {
+    abstract val name: String
+    abstract val version: String
+    abstract val url: String
+
+    open val dependencies: List<String> = emptyList()
+    abstract val modules: List<Module>
+
+    open fun fetchSource(
+        sourceDirectory: File,
+        workingDirectory: File
+    ): Result<Unit, String>
+
+    open fun configure(
+        toolchain: Toolchain,
+        sourceDirectory: File,
+        buildDirectory: File,
+        installDirectory: File,
+        workingDirectory: File
+    ): Result<Unit, String>
+
+    open fun build(
+        toolchain: Toolchain,
+        buildDirectory: File
+    ): Result<Unit, String>
+
+    open fun install(
+        toolchain: Toolchain,
+        buildDirectory: File,
+        installDirectory: File
+    ): Result<Unit, String>
+}
+```
+
+Individual port files are kept in `ports/$name/port.kts`. For example, the cURL
+port is [ports/curl/port.kts](ports/curl/port.kts).
+
+## Building a Port
+
+ndkports requires an NDK to be used for building to be specified on the command
+line as well as a list of packages to build. For example, to build cURL:
+
+```bash
+$ ./gradlew run --args='--ndk /path/to/android-ndk-r20 openssl curl'
+Build output...
+$ find  -name '*.aar'
+./out/curl/curl.aar
+./out/openssl/openssl.aar
+```
+
+Note that dependencies currently need to be already built or ordered explicitly.
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..b9a4d11
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,60 @@
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+    kotlin("jvm") version "1.3.50"
+    application
+}
+
+group = "com.android"
+version = "1.0.0-SNAPSHOT"
+
+repositories {
+    mavenCentral()
+    jcenter()
+    google()
+    maven(url = "https://dl.bintray.com/s1m0nw1/KtsRunner")
+}
+
+dependencies {
+    implementation(kotlin("stdlib", "1.3.50"))
+    implementation(kotlin("reflect", "1.3.50"))
+
+    implementation("com.google.prefab:api:1.0.0-alpha2")
+
+    implementation("com.github.ajalt:clikt:2.2.0")
+    implementation("com.squareup.okhttp3:okhttp:4.2.2")
+    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"
+    )
+}
+
+// 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") {
+    val allPorts = File("ports").listFiles()!!.map { it.name }
+    args = listOf("--ndk", ndkPath, "-o", "out") + allPorts
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..29e08e8
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1 @@
+kotlin.code.style=official
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..94336fc
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..7c4388a
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+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=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+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
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    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
+    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
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((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" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+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
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@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=
+
+@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
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+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%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/ports/curl/port.kts b/ports/curl/port.kts
new file mode 100644
index 0000000..4a47446
--- /dev/null
+++ b/ports/curl/port.kts
@@ -0,0 +1,57 @@
+/*
+ * 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.66.0"
+    override val url = "https://curl.haxx.se/download/curl-$version.tar.gz"
+    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"
+        )
+    }
+}
\ No newline at end of file
diff --git a/ports/jsoncpp/port.kts b/ports/jsoncpp/port.kts
new file mode 100644
index 0000000..f951e23
--- /dev/null
+++ b/ports/jsoncpp/port.kts
@@ -0,0 +1,46 @@
+/*
+ * 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.9.1"
+    override val url =
+        "https://github.com/open-source-parsers/jsoncpp/archive/$version.tar.gz"
+
+    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 fetchSource(
+        sourceDirectory: File,
+        workingDirectory: File
+    ): Result<Unit, String> =
+        super.fetchSource(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()
+        }
+}
\ No newline at end of file
diff --git a/ports/openssl/port.kts b/ports/openssl/port.kts
new file mode 100644
index 0000000..00345bc
--- /dev/null
+++ b/ports/openssl/port.kts
@@ -0,0 +1,90 @@
+/*
+ * 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.1d"
+    override val prefabVersion = CMakeCompatibleVersion(1, 1, 1, 4)
+    override val url = "https://www.openssl.org/source/openssl-$version.tar.gz"
+
+    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")}"
+            )
+        )
+}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..4549d2e
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,11 @@
+rootProject.name = "ndkports"
+
+pluginManagement {
+    resolutionStrategy {
+        eachPlugin {
+            if (requested.id.id == "kotlinx-serialization") {
+                useModule("org.jetbrains.kotlin:kotlin-serialization:${requested.version}")
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/android/ndkports/Abi.kt b/src/main/kotlin/com/android/ndkports/Abi.kt
new file mode 100644
index 0000000..58981a2
--- /dev/null
+++ b/src/main/kotlin/com/android/ndkports/Abi.kt
@@ -0,0 +1,24 @@
+/*
+ * 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
new file mode 100644
index 0000000..053fe8d
--- /dev/null
+++ b/src/main/kotlin/com/android/ndkports/AutoconfPort.kt
@@ -0,0 +1,69 @@
+/*
+ * 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()
+
+    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 = mapOf(
+                "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")}"
+            )
+        )
+    }
+
+    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/CMakeCompatibleVersion.kt b/src/main/kotlin/com/android/ndkports/CMakeCompatibleVersion.kt
new file mode 100644
index 0000000..6d3ee45
--- /dev/null
+++ b/src/main/kotlin/com/android/ndkports/CMakeCompatibleVersion.kt
@@ -0,0 +1,66 @@
+/*
+ * 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
+
+/**
+ * A version number that is compatible with CMake's package version format.
+ *
+ * https://cmake.org/cmake/help/latest/manual/cmake-packages.7.html#package-version-file
+ *
+ * CMake package versions *must* be numeric with a maximum of four dot separated
+ * components.
+ */
+data class CMakeCompatibleVersion(
+    val major: Int,
+    val minor: Int?,
+    val patch: Int?,
+    val tweak: Int?
+) {
+    init {
+        if (tweak != null) {
+            require(patch != null)
+        }
+
+        if (patch != null) {
+            require(minor != null)
+        }
+    }
+
+    override fun toString(): String =
+        listOfNotNull(major, minor, patch, tweak).joinToString(".")
+
+    companion object {
+        private val versionRegex = Regex(
+            """^(\d+)(?:\.(\d+)(?:\.(\d+)(?:\.(\d+))?)?)?$"""
+        )
+
+        fun parse(versionString: String): CMakeCompatibleVersion {
+            val match = versionRegex.find(versionString)
+            require(match != null) {
+                "$versionString is not in major[.minor[.patch[.tweak]]] format"
+            }
+            return CMakeCompatibleVersion(
+                match.groups[1]!!.value.toInt(),
+                match.groups[2]?.value?.toInt(),
+                match.groups[3]?.value?.toInt(),
+                match.groups[4]?.value?.toInt()
+            )
+        }
+    }
+}
\ 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
new file mode 100644
index 0000000..25d48ed
--- /dev/null
+++ b/src/main/kotlin/com/android/ndkports/Cli.kt
@@ -0,0 +1,129 @@
+/*
+ * 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 loadPort(name: String): Port {
+        val portDir = File("ports").resolve(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")
+
+            port.fetchSource(sourceDirectory, workingDirectory).onFailure {
+                println(it)
+                exitProcess(1)
+            }
+
+            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
new file mode 100644
index 0000000..8ec639f
--- /dev/null
+++ b/src/main/kotlin/com/android/ndkports/License.kt
@@ -0,0 +1,19 @@
+/*
+ * 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/MesonPort.kt b/src/main/kotlin/com/android/ndkports/MesonPort.kt
new file mode 100644
index 0000000..8c1f706
--- /dev/null
+++ b/src/main/kotlin/com/android/ndkports/MesonPort.kt
@@ -0,0 +1,96 @@
+/*
+ * 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 MesonPort : Port() {
+    enum class DefaultLibraryType(val argument: String) {
+        Both("both"),
+        Shared("shared"),
+        Static("static")
+    }
+
+    open val defaultLibraryType: DefaultLibraryType = DefaultLibraryType.Shared
+
+    override fun configure(
+        toolchain: Toolchain,
+        sourceDirectory: File,
+        buildDirectory: File,
+        installDirectory: File,
+        workingDirectory: File
+    ): Result<Unit, String> {
+        val cpuFamily = when (toolchain.abi) {
+            Abi.Arm -> "arm"
+            Abi.Arm64 -> "aarch64"
+            Abi.X86 -> "x86"
+            Abi.X86_64 -> "x86_64"
+        }
+
+        val cpu = when (toolchain.abi) {
+            Abi.Arm -> "armv7a"
+            Abi.Arm64 -> "armv8a"
+            Abi.X86 -> "i686"
+            Abi.X86_64 -> "x86_64"
+        }
+
+        val crossFile = workingDirectory.resolve("cross_file.txt").apply {
+            writeText("""
+            [binaries]
+            ar = '${toolchain.ar}'
+            c = '${toolchain.clang}'
+            cpp = '${toolchain.clangxx}'
+            strip = '${toolchain.strip}'
+
+            [host_machine]
+            system = 'android'
+            cpu_family = '$cpuFamily'
+            cpu = '$cpu'
+            endian = 'little'
+            """.trimIndent())
+        }
+
+        return executeProcessStep(
+            listOf(
+                "meson",
+                "--cross-file",
+                crossFile.absolutePath,
+                "--buildtype",
+                "release",
+                "--prefix",
+                installDirectory.absolutePath,
+                "--default-library",
+                defaultLibraryType.argument,
+                sourceDirectory.absolutePath,
+                buildDirectory.absolutePath
+            ), workingDirectory
+        )
+    }
+
+    override fun build(
+        toolchain: Toolchain,
+        buildDirectory: File
+    ): Result<Unit, String> =
+        executeProcessStep(listOf("ninja", "-v"), buildDirectory)
+
+    override fun install(
+        toolchain: Toolchain,
+        buildDirectory: File,
+        installDirectory: File
+    ): Result<Unit, String> =
+        executeProcessStep(listOf("ninja", "-v", "install"), buildDirectory)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/android/ndkports/Ndk.kt b/src/main/kotlin/com/android/ndkports/Ndk.kt
new file mode 100644
index 0000000..af66cb5
--- /dev/null
+++ b/src/main/kotlin/com/android/ndkports/Ndk.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.lang.RuntimeException
+
+class Ndk(val path: File) {
+    val version = NdkVersion.fromNdk(path)
+
+    private val llvmBaseDir = path.resolve("toolchains/llvm/prebuilt")
+    val hostTag: String = llvmBaseDir.let {
+        val files = it.list()
+            ?: throw RuntimeException("Unable to get file list for $it")
+
+        if (files.size != 1) {
+            throw RuntimeException("Expected exactly one directory in $it")
+        }
+
+        files.first()
+    }
+
+    private val toolchainDirectory = llvmBaseDir.resolve(hostTag)
+    val toolchainBinDirectory = toolchainDirectory.resolve("bin")
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/android/ndkports/NdkVersion.kt b/src/main/kotlin/com/android/ndkports/NdkVersion.kt
new file mode 100644
index 0000000..fce1304
--- /dev/null
+++ b/src/main/kotlin/com/android/ndkports/NdkVersion.kt
@@ -0,0 +1,58 @@
+/*
+ * 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
+
+data class NdkVersion(
+    val major: Int,
+    val minor: Int,
+    val build: Int,
+    val qualifier: String?
+) {
+    companion object {
+        private val pkgRevisionRegex = Regex("""^Pkg.Revision\s*=\s*(\S+)$""")
+        private val versionRegex = Regex("""^(\d+).(\d+).(\d+)(?:-(\S+))?$""")
+
+        private fun fromString(versionString: String): NdkVersion {
+            val match = versionRegex.find(versionString)
+            require(match != null) { "Invalid version string" }
+            val (major, minor, build, qualifier) = match.destructured
+            return NdkVersion(
+                major.toInt(),
+                minor.toInt(),
+                build.toInt(),
+                qualifier.takeIf { match.groups[4] != null }
+            )
+        }
+
+        fun fromSourcePropertiesText(text: String): NdkVersion {
+            for (line in text.lines().map { it.trim() }) {
+                pkgRevisionRegex.find(line)?.let {
+                    return fromString(it.groups.last()!!.value)
+                }
+            }
+            throw RuntimeException(
+                "Did not find Pkg.Revision in source.properties"
+            )
+        }
+
+        fun fromNdk(ndk: File): NdkVersion = fromSourcePropertiesText(
+            ndk.resolve("source.properties").readText()
+        )
+    }
+}
\ 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
new file mode 100644
index 0000000..08c8fbf
--- /dev/null
+++ b/src/main/kotlin/com/android/ndkports/Port.kt
@@ -0,0 +1,158 @@
+/*
+ * 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 okhttp3.OkHttpClient
+import okhttp3.Request
+import java.io.File
+import java.io.FileOutputStream
+
+@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}")
+
+data class Module(
+    val name: String,
+    val includesPerAbi: Boolean = false,
+    val dependencies: List<String> = emptyList()
+)
+
+abstract class Port {
+    abstract val name: String
+    abstract val version: String
+    open val prefabVersion: CMakeCompatibleVersion
+        get() = CMakeCompatibleVersion.parse(version)
+    open val mavenVersion: String
+        get() = version
+
+    abstract val url: String
+
+    open val licensePath: String = "LICENSE"
+
+    abstract val license: License
+
+    open val dependencies: List<String> = emptyList()
+    abstract val modules: List<Module>
+
+    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)
+    }
+
+    open fun fetchSource(
+        sourceDirectory: File,
+        workingDirectory: File
+    ): Result<Unit, String> {
+        val file = workingDirectory.resolve(File(url).name)
+
+        val client = OkHttpClient()
+        val request = Request.Builder().url(url).build()
+        client.newCall(request).execute().use { response ->
+            if (!response.isSuccessful) {
+                return Result.Error("Failed to download $url")
+            }
+
+            val body = response.body ?: throw RuntimeException(
+                "Expected non-null response body for $url"
+            )
+            FileOutputStream(file).use { output ->
+                body.byteStream().use { input ->
+                    input.copyTo(output)
+                }
+            }
+        }
+
+        sourceDirectory.mkdirs()
+        return executeProcessStep(
+            listOf(
+                "tar",
+                "xf",
+                file.absolutePath,
+                "--strip-components=1"
+            ), sourceDirectory
+        )
+    }
+
+    open fun configure(
+        toolchain: Toolchain,
+        sourceDirectory: File,
+        buildDirectory: File,
+        installDirectory: File,
+        workingDirectory: File
+    ): Result<Unit, String> = Result.Ok(Unit)
+
+    open fun build(
+        toolchain: Toolchain,
+        buildDirectory: File
+    ): Result<Unit, String> = Result.Ok(Unit)
+
+    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
new file mode 100644
index 0000000..cac9acd
--- /dev/null
+++ b/src/main/kotlin/com/android/ndkports/PrefabPackageBuilder.kt
@@ -0,0 +1,253 @@
+/*
+ * 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}.aar")
+        val pomFile = directory.resolve("${port.name}.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
new file mode 100644
index 0000000..21c203e
--- /dev/null
+++ b/src/main/kotlin/com/android/ndkports/Result.kt
@@ -0,0 +1,36 @@
+/*
+ * 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/Toolchain.kt b/src/main/kotlin/com/android/ndkports/Toolchain.kt
new file mode 100644
index 0000000..d42e8d1
--- /dev/null
+++ b/src/main/kotlin/com/android/ndkports/Toolchain.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
+
+class Toolchain(val ndk: Ndk, val abi: Abi, val api: Int) {
+    val binutilsTriple = when (abi) {
+        Abi.Arm -> "arm-linux-androideabi"
+        Abi.Arm64 -> "aarch64-linux-android"
+        Abi.X86 -> "i686-linux-android"
+        Abi.X86_64 -> "x86_64-linux-android"
+    }
+
+    private val clangTriple = when (abi) {
+        Abi.Arm -> "armv7a-linux-androideabi$api"
+        else -> "$binutilsTriple$api"
+    }
+
+    val binDir = ndk.toolchainBinDirectory
+    val ar = binDir.resolve("$binutilsTriple-ar")
+    val clang = binDir.resolve("$clangTriple-clang")
+    val clangxx = binDir.resolve("$clangTriple-clang++")
+    val ranlib = binDir.resolve("$binutilsTriple-ranlib")
+    val strip = binDir.resolve("$binutilsTriple-strip")
+}
\ 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
new file mode 100644
index 0000000..a23ff90
--- /dev/null
+++ b/src/main/kotlin/com/android/ndkports/Zip.kt
@@ -0,0 +1,53 @@
+/*
+ * 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/CMakeCompatibleVersionTest.kt b/src/test/kotlin/com/android/ndkports/CMakeCompatibleVersionTest.kt
new file mode 100644
index 0000000..7724a8e
--- /dev/null
+++ b/src/test/kotlin/com/android/ndkports/CMakeCompatibleVersionTest.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.junit.jupiter.api.assertThrows
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class CMakeCompatibleVersionTest {
+    @Test
+    fun `can parse version number`() {
+        assertEquals(
+            CMakeCompatibleVersion(1, null, null, null),
+            CMakeCompatibleVersion.parse("1")
+        )
+        assertEquals(
+            CMakeCompatibleVersion(2, 1, null, null),
+            CMakeCompatibleVersion.parse("2.1")
+        )
+        assertEquals(
+            CMakeCompatibleVersion(3, 2, 1, null),
+            CMakeCompatibleVersion.parse("3.2.1")
+        )
+        assertEquals(
+            CMakeCompatibleVersion(4, 3, 2, 1),
+            CMakeCompatibleVersion.parse("4.3.2.1")
+        )
+    }
+
+    @Test
+    fun `reject invalid versions`() {
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion.parse("")
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion.parse(" ")
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion.parse("1.")
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion.parse(".1")
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion.parse(".1.")
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion.parse(" 1")
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion.parse("1 ")
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion.parse(" 1 ")
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion.parse("2.1.")
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion.parse(".2.1")
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion.parse(".2.1.")
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion.parse("1a")
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion.parse("2b.1")
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion.parse("5.4.3.2.1")
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion.parse("4.3.2.1a")
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion.parse("2.a.1")
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion.parse("3. .1")
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion.parse("1..2")
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion.parse(".")
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion.parse("...")
+        }
+    }
+
+    @Test
+    fun `constructor requires that nulls come last`() {
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion(1, 2, null, 3)
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion(1, null, 2, 3)
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion(1, null, 2, null)
+        }
+        assertThrows<IllegalArgumentException> {
+            CMakeCompatibleVersion(1, null, null, 2)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/com/android/ndkports/NdkVersionTest.kt b/src/test/kotlin/com/android/ndkports/NdkVersionTest.kt
new file mode 100644
index 0000000..750cc52
--- /dev/null
+++ b/src/test/kotlin/com/android/ndkports/NdkVersionTest.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.junit.jupiter.api.assertThrows
+import java.lang.RuntimeException
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class NdkVersionTest {
+    @Test
+    fun `can parse source properties`() {
+        assertEquals(
+            NdkVersion(20, 0, 5594570, null),
+            NdkVersion.fromSourcePropertiesText(
+                """
+                Pkg.Desc = Android NDK
+                Pkg.Revision = 20.0.5594570
+                """.trimIndent()
+            )
+        )
+
+        assertEquals(
+            NdkVersion(20, 0, 5594570, "canary"),
+            NdkVersion.fromSourcePropertiesText(
+                """
+                Pkg.Revision = 20.0.5594570-canary
+                Pkg.Desc = Android NDK
+                """.trimIndent()
+            )
+        )
+
+        assertEquals(
+            NdkVersion(20, 0, 5594570, "beta2"),
+            NdkVersion.fromSourcePropertiesText(
+                """
+
+                    Pkg.Revision     =     20.0.5594570-beta2    
+                Pkg.Desc = Android NDK
+
+                """.trimIndent()
+            )
+        )
+        assertEquals(
+            NdkVersion(20, 0, 5594570, "rc1"),
+            NdkVersion.fromSourcePropertiesText(
+                """
+                Pkg.Desc = Android NDK
+
+
+
+                Pkg.Revision = 20.0.5594570-rc1
+                """.trimIndent()
+            )
+        )
+    }
+
+    @Test
+    fun `fails if not found`() {
+        assertThrows<RuntimeException> {
+            NdkVersion.fromSourcePropertiesText("Pkg.Desc = Android NDK")
+        }
+    }
+}
\ 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
new file mode 100644
index 0000000..0abef18
--- /dev/null
+++ b/src/test/kotlin/com/android/ndkports/ResultTest.kt
@@ -0,0 +1,67 @@
+/*
+ * 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
diff --git a/src/test/resources/junit-platform.properties b/src/test/resources/junit-platform.properties
new file mode 100644
index 0000000..d265fd8
--- /dev/null
+++ b/src/test/resources/junit-platform.properties
@@ -0,0 +1 @@
+junit.jupiter.testinstance.lifecycle.default = per_class