Skip to content

Commit

Permalink
Add support for passing in APKS files externally
Browse files Browse the repository at this point in the history
  • Loading branch information
nathan3d committed Jan 8, 2024
1 parent a8b4849 commit 2e1be8f
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 40 deletions.
68 changes: 43 additions & 25 deletions ruler-cli/src/main/java/com/spotify/ruler/cli/RulerCli.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import com.spotify.ruler.common.BaseRulerTask
import com.spotify.ruler.common.FEATURE_NAME
import com.spotify.ruler.common.apk.ApkCreator
import com.spotify.ruler.common.apk.InjectedToolApkCreator
import com.spotify.ruler.common.apk.parseSplitApkDirectory
import com.spotify.ruler.common.apk.unzipFile
import com.spotify.ruler.common.dependency.ArtifactResult
import com.spotify.ruler.common.dependency.DependencyComponent
import com.spotify.ruler.common.dependency.DependencyEntry
Expand All @@ -40,14 +42,16 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import java.io.File
import java.nio.file.Files
import java.util.logging.Level
import java.util.logging.Logger

class RulerCli : CliktCommand(), BaseRulerTask {
private val logger = Logger.getLogger("Ruler")
private val dependencyMap by option().file().required()
private val rulerConfigJson by option().file().required()
private val apkFile by option().file().required()
private val apkFile by option().file()
private val bundleFile by option().file()
private val reportDir by option().file(canBeDir = true).required()
private val mappingFile: File? by option().file()
private val resourceMappingFile: File? by option().file()
Expand All @@ -69,7 +73,7 @@ class RulerCli : CliktCommand(), BaseRulerTask {
val json = Json.decodeFromStream<JsonRulerConfig>(rulerConfigJson.inputStream())
RulerConfig(
projectPath = json.projectPath,
apkFilesMap = createApkFile(json.projectPath, json.deviceSpec!!),
apkFilesMap = apkFiles(config = json),
reportDir = reportDir,
ownershipFile = json.ownershipFile?.let { File(it) },
staticDependenciesFile = json.staticComponentsPath?.let { File(it) },
Expand Down Expand Up @@ -106,14 +110,50 @@ class RulerCli : CliktCommand(), BaseRulerTask {
dependencySanitizer.sanitize(entries)
}

private fun apkFiles(config: JsonRulerConfig): Map<String, List<File>> {
return if (apkFile != null) {
if (apkFile!!.extension == "apk") {
logger.log(Level.INFO, "Using APK file ${apkFile?.path}")
mapOf(FEATURE_NAME to listOf(apkFile!!))
} else {
logger.log(Level.INFO, "Using Split APK file ${apkFile?.path}")
val directory = Files.createTempDirectory("split_apk_tmp")
unzipFile(apkFile!!, directory)
parseSplitApkDirectory(directory.toFile())
}
} else if (bundleFile != null) {
with(if (aapt2Tool != null) {
logger.log(
Level.INFO,
"Creating InjectedToolApkCreator with ${aapt2Tool?.path}"
)
InjectedToolApkCreator(aapt2Tool!!.toPath())
} else {
ApkCreator(File(config.projectPath))
}
) {
createSplitApks(
bundleFile!!,
config.deviceSpec!!,
File(config.projectPath).resolve(File("tmp")).apply {
mkdir()
}
)
}
} else {
throw IllegalArgumentException("No APK file or bundle file provided")
}
}

override fun provideDependencies(): Map<String, List<DependencyComponent>> = dependencies

override fun run() {
logger.log(Level.INFO, """
~~~~~ Starting Ruler ~~~~~
Using Dependency Map: ${dependencyMap.path}
Using Ruler Config: ${rulerConfigJson.path}
Using App File: ${apkFile.path}
Using APK File: ${apkFile?.path}
Using Bundle File: ${bundleFile?.path}
Using Proguard Mapping File: ${mappingFile?.path}
Using Resource Mapping File: ${resourceMappingFile?.path}
Using AAPT2: ${aapt2Tool?.path}
Expand All @@ -122,28 +162,6 @@ class RulerCli : CliktCommand(), BaseRulerTask {
""".trimIndent())
super.run()
}

private fun createApkFile(projectPath: String, deviceSpec: DeviceSpec): Map<String, List<File>> {

val apkCreator = if (aapt2Tool != null) {
InjectedToolApkCreator(aapt2Tool!!.toPath())
} else {
ApkCreator(File(projectPath))
}

return if (apkFile.extension == "apk") {
mapOf(FEATURE_NAME to listOf(apkFile))
} else {
apkCreator.createSplitApks(
apkFile,
deviceSpec,
File(projectPath).resolve(File("tmp")).apply {
mkdir()
}
)
}
}

}

@Serializable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

/*
* Copyright 2021 Spotify AB
*
Expand Down Expand Up @@ -33,20 +32,24 @@ import com.android.utils.StdLogger
import com.spotify.ruler.common.models.DeviceSpec
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.BufferedReader
import java.io.File
import java.io.IOException
import java.io.InputStreamReader
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.StringReader
import java.nio.file.Files
import java.nio.file.Path
import java.util.Optional
import java.util.concurrent.TimeUnit
import java.util.logging.Level
import java.util.logging.Logger
import java.util.zip.ZipInputStream

/**
* Responsible for creating APKs based on provided app bundle (AAB) files.
*
* @param rootDir Root directory of the Gradle project, needed to look up the path of certain binaries.
*/

const val BUFFER_SIZE = 1024
open class ApkCreator(private val rootDir: File) {

private val rulerDebugKey = "rulerDebug.keystore"
Expand All @@ -61,7 +64,11 @@ open class ApkCreator(private val rootDir: File) {
* @param targetDir Directory where the APKs should be located. Contents of this directory will be deleted
* @return Map of modules from the AAB file with all the APKs belonging to each module
*/
fun createSplitApks(bundleFile: File, deviceSpec: DeviceSpec, targetDir: File): Map<String, List<File>> {
fun createSplitApks(
bundleFile: File,
deviceSpec: DeviceSpec,
targetDir: File
): Map<String, List<File>> {
targetDir.listFiles()?.forEach(File::deleteRecursively) // Overwrite existing files

BuildApksCommand.builder()
Expand All @@ -74,14 +81,7 @@ open class ApkCreator(private val rootDir: File) {
.build()
.execute()

val result = BuildApksResult.parseFrom(targetDir.resolve("toc.pb").readBytes())
val variant = result.variantList.single() // We're targeting one device -> we only expect a single variant

return variant.apkSetList.associate { apkSet ->
val moduleName = apkSet.moduleMetadata.name
val moduleSplits = apkSet.apkDescriptionList.map { targetDir.resolve(it.path) }
moduleName to moduleSplits
}
return parseSplitApkDirectory(targetDir)
}

/** Converts the given [deviceSpec] into a format which bundletool understands. */
Expand Down Expand Up @@ -134,6 +134,62 @@ open class ApkCreator(private val rootDir: File) {
}
}

class InjectedToolApkCreator(private val aapt2Tool: Path): ApkCreator(File("")) {
class InjectedToolApkCreator(private val aapt2Tool: Path) : ApkCreator(File("")) {
override fun getAapt2Location(): Path = aapt2Tool
}

@Suppress("NestedBlockDepth")
fun unzipFile(zipFile: File, destDirectory: Path) {
val logger = Logger.getLogger("Ruler")
val buffer = ByteArray(BUFFER_SIZE)

// Create a temporary directory
Files.createDirectories(destDirectory)

// Create ZipInputStream to read the zip file
val zipInputStream = ZipInputStream(FileInputStream(zipFile))

// Loop through each entry in the zip file
var zipEntry = zipInputStream.nextEntry
while (zipEntry != null) {
val newFile = destDirectory.resolve(zipEntry.name)
logger.log(Level.INFO, "extracting $zipEntry to $newFile")

// Create necessary directories if they don't exist
if (zipEntry.isDirectory) {
Files.createDirectories(newFile)
} else {
newFile.toFile().parentFile.mkdirs()
// Create FileOutputStream to write the file
FileOutputStream(newFile.toFile()).use { fos ->
// Read and write the data
var len = zipInputStream.read(buffer)
while (len > 0) {
fos.write(buffer, 0, len)
len = zipInputStream.read(buffer)
}
}
}

// Move to the next entry in the zip file
zipEntry = zipInputStream.nextEntry
}

// Close the ZipInputStream
zipInputStream.closeEntry()
zipInputStream.close()

println("File successfully unzipped to $destDirectory")
}

fun parseSplitApkDirectory(targetDir: File): Map<String, List<File>> {
val result = BuildApksResult.parseFrom(targetDir.resolve("toc.pb").readBytes())
val variant =
result.variantList.single() // We're targeting one device -> we only expect a single variant

return variant.apkSetList.associate { apkSet ->
val moduleName = apkSet.moduleMetadata.name
val moduleSplits = apkSet.apkDescriptionList.map { targetDir.resolve(it.path) }
moduleName to moduleSplits
}
}

0 comments on commit 2e1be8f

Please sign in to comment.