Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support verification of download and install size #168

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion ruler-cli/src/main/java/com/spotify/ruler/cli/RulerCli.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.github.ajalt.clikt.parameters.options.multiple
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 com.github.ajalt.clikt.parameters.types.long
import com.spotify.ruler.common.BaseRulerTask
import com.spotify.ruler.common.FEATURE_NAME
import com.spotify.ruler.common.apk.ApkCreator
Expand All @@ -40,6 +41,7 @@ import com.spotify.ruler.common.models.AppInfo
import com.spotify.ruler.common.models.DeviceSpec
import com.spotify.ruler.common.models.RulerConfig
import com.spotify.ruler.common.sanitizer.ClassNameSanitizer
import com.spotify.ruler.common.veritication.VerificationConfig
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
Expand Down Expand Up @@ -69,6 +71,8 @@ class RulerCli : CliktCommand(), BaseRulerTask {
private val appInfoFile by option().file().required()
private val additionalEntriesFile by option().file()
private val ignoreFile by option().multiple()
private val downloadSizeThreshold by option().long().default(Long.MAX_VALUE)
private val installSizeThreshold by option().long().default(Long.MAX_VALUE)

override fun print(content: String) = echo(content)

Expand All @@ -89,6 +93,9 @@ class RulerCli : CliktCommand(), BaseRulerTask {
val additionalEntries =
additionalEntriesFile?.let { Json.decodeFromStream<List<ApkEntry.Default>>(it.inputStream()) }
logger.log(Level.INFO, "Got ${additionalEntries?.size} additional entries")

val verificationConfig = VerificationConfig(downloadSizeThreshold, installSizeThreshold)

RulerConfig(
projectPath = projectPath,
apkFilesMap = apkFiles(projectPath, deviceSpec),
Expand All @@ -100,7 +107,8 @@ class RulerCli : CliktCommand(), BaseRulerTask {
defaultOwner = defaultOwner,
omitFileBreakdown = omitFileBreakdown,
additionalEntries = additionalEntries,
ignoredFiles = ignoreFile
ignoredFiles = ignoreFile,
verificationConfig = verificationConfig
)
}

Expand Down Expand Up @@ -181,6 +189,8 @@ class RulerCli : CliktCommand(), BaseRulerTask {
Using AAPT2: ${aapt2Tool?.path}
Using Bloaty: ${bloatyTool?.path}
Writing reports to: ${reportDir.path}
Verifying download size under: $downloadSizeThreshold
Verifying install size under: $installSizeThreshold
""".trimIndent()
)
super.run()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.spotify.ruler.common.report.JsonReporter
import com.spotify.ruler.common.sanitizer.ClassNameSanitizer
import com.spotify.ruler.common.sanitizer.ResourceNameSanitizer
import com.spotify.ruler.common.util.toEscapeCharRegex
import com.spotify.ruler.common.veritication.Verificator
import com.spotify.ruler.models.AppFile
import com.spotify.ruler.models.ComponentType
import kotlinx.serialization.decodeFromString
Expand Down Expand Up @@ -84,6 +85,9 @@ interface BaseRulerTask {

val ownershipInfo = getOwnershipInfo() // Get ownership information for all components
generateReports(components, featureFiles, ownershipInfo)

val verificator = rulerConfig.verificationConfig.let(::Verificator)
verificator.verify(components.values.flatten())
}

private fun getFilesFromBundle(): Map<String, List<AppFile>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.spotify.ruler.common.models

import com.spotify.ruler.common.apk.ApkEntry
import com.spotify.ruler.common.veritication.VerificationConfig
import java.io.File

data class RulerConfig(
Expand All @@ -30,5 +31,6 @@ data class RulerConfig(
val defaultOwner: String,
val omitFileBreakdown: Boolean,
val additionalEntries: List<ApkEntry.Default>?,
val ignoredFiles: List<String>
val ignoredFiles: List<String>,
val verificationConfig: VerificationConfig
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2024 Spotify AB
*
* 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.spotify.ruler.common.veritication

class SizeExceededException(label: String, size: Long, threshold: Long) :
Exception("$label size exceeds the threshold by ${size - threshold} bytes.")
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2024 Spotify AB
*
* 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.spotify.ruler.common.veritication

import java.io.Serializable
import kotlinx.serialization.Serializable as KSerializable

@KSerializable
data class VerificationConfig(
val downloadSizeThreshold: Long = Long.MAX_VALUE,
val installSizeThreshold: Long = Long.MAX_VALUE
) : Serializable {
companion object {
private const val serialVersionUID = 1L
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2024 Spotify AB
*
* 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.spotify.ruler.common.veritication

import com.spotify.ruler.models.AppFile

class Verificator(private val config: VerificationConfig) {

fun verify(components: List<AppFile>) {
val downloadSize = components.sumOf(AppFile::downloadSize)
val downloadSizeThreshold = config.downloadSizeThreshold
if (downloadSize > downloadSizeThreshold) {
throw SizeExceededException("Download", downloadSize, downloadSizeThreshold)
}

val installSize = components.sumOf(AppFile::installSize)
val installSizeThreshold = config.installSizeThreshold
if (installSize > installSizeThreshold) {
throw SizeExceededException("Install", installSize, installSizeThreshold)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright 2024 Spotify AB
*
* 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.spotify.ruler.common.verificator

import com.spotify.ruler.common.veritication.SizeExceededException
import com.spotify.ruler.common.veritication.VerificationConfig
import com.spotify.ruler.common.veritication.Verificator
import com.spotify.ruler.models.AppFile
import com.spotify.ruler.models.FileType
import com.spotify.ruler.models.ResourceType
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows

class VerificatorTest {
private val config = VerificationConfig(100, 100)
private val verificator = Verificator(config)

@Test
fun `Download size under threshold does not trigger SizeExceededException`() {
val downloadSize = config.downloadSizeThreshold / 2
val appFiles = generateAppFiles(downloadSize)

assertDoesNotThrow { verificator.verify(appFiles) }
}

@Test
fun `Download size exceeding threshold triggers SizeExceededException`() {
val downloadSize = config.downloadSizeThreshold * 2
val appFiles = generateAppFiles(downloadSize)

assertThrows<SizeExceededException> { verificator.verify(appFiles) }
}

@Test
fun `Install size under threshold does not trigger SizeExceededException`() {
val installSize = config.installSizeThreshold / 2
val appFiles = generateAppFiles(config.downloadSizeThreshold, installSize)

assertDoesNotThrow { verificator.verify(appFiles) }
}

@Test
fun `Install size exceeding threshold triggers SizeExceededException`() {
val installSize = config.downloadSizeThreshold * 2
val appFiles = generateAppFiles(config.downloadSizeThreshold, installSize)

assertThrows<SizeExceededException> { verificator.verify(appFiles) }
}

private fun generateAppFiles(
downloadSize: Long,
installSize: Long = downloadSize
): List<AppFile> {
val downloadSizePerFile = downloadSize / 2
val installSizePerFile = installSize / 2
return listOf(
AppFile(
"com.spotify.MainActivity",
FileType.CLASS,
downloadSizePerFile,
installSizePerFile
),
AppFile(
"/res/layout/activity_main.xml",
FileType.RESOURCE,
downloadSizePerFile,
installSizePerFile,
resourceType = ResourceType.LAYOUT
),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.ApplicationVariant
import com.spotify.ruler.common.models.AppInfo
import com.spotify.ruler.common.models.DeviceSpec
import com.spotify.ruler.common.veritication.VerificationConfig
import org.codehaus.groovy.runtime.StringGroovyMethods
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.file.RegularFile
import org.gradle.api.plugins.ExtensionAware
import org.gradle.api.provider.Provider

class RulerPlugin : Plugin<Project> {
Expand All @@ -34,6 +36,10 @@ class RulerPlugin : Plugin<Project> {
@Suppress("UnstableApiUsage")
override fun apply(project: Project) {
val rulerExtension = project.extensions.create(name, RulerExtension::class.java)
val rulerVerificationExtension = (rulerExtension as ExtensionAware).extensions.create(
"verification",
RulerVerificationExtension::class.java
)

project.plugins.withId("com.android.application") {
val androidComponents =
Expand Down Expand Up @@ -61,6 +67,8 @@ class RulerPlugin : Plugin<Project> {
task.omitFileBreakdown.set(rulerExtension.omitFileBreakdown)
task.unstrippedNativeFiles.set(rulerExtension.unstrippedNativeFiles)

task.verificationConfig.set(getVerificationConfig(rulerVerificationExtension))

// Add explicit dependency to support DexGuard
task.dependsOn("bundle$variantName")
}
Expand Down Expand Up @@ -161,6 +169,13 @@ class RulerPlugin : Plugin<Project> {
}
}

private fun getVerificationConfig(extension: RulerVerificationExtension): VerificationConfig {
return VerificationConfig(
downloadSizeThreshold = extension.downloadSizeThreshold.get(),
installSizeThreshold = extension.installSizeThreshold.get()
)
}

/** Checks if the given [project] is using DexGuard for obfuscation, instead of R8. */
private fun hasDexGuard(project: Project): Boolean {
return project.pluginManager.hasPlugin("dexguard")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.spotify.ruler.common.models.AppInfo
import com.spotify.ruler.common.models.DeviceSpec
import com.spotify.ruler.common.models.RulerConfig
import com.spotify.ruler.common.sanitizer.ClassNameSanitizer
import com.spotify.ruler.common.veritication.VerificationConfig
import com.spotify.ruler.plugin.dependency.EntryParser
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
Expand Down Expand Up @@ -77,6 +78,9 @@ abstract class RulerTask : DefaultTask(), BaseRulerTask {
@get:InputFile
abstract val staticDependenciesFile: RegularFileProperty

@get:Input
abstract val verificationConfig: Property<VerificationConfig>

@get:OutputDirectory
abstract val workingDir: DirectoryProperty

Expand All @@ -100,7 +104,8 @@ abstract class RulerTask : DefaultTask(), BaseRulerTask {
defaultOwner = defaultOwner.get(),
omitFileBreakdown = omitFileBreakdown.get(),
additionalEntries = emptyList(),
ignoredFiles = emptyList()
ignoredFiles = emptyList(),
verificationConfig = verificationConfig.get()
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2024 Spotify AB
*
* 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.spotify.ruler.plugin

import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property

open class RulerVerificationExtension(objects: ObjectFactory) {
val downloadSizeThreshold: Property<Long> = objects.property(Long::class.java)
val installSizeThreshold: Property<Long> = objects.property(Long::class.java)

init {
downloadSizeThreshold.convention(Long.MAX_VALUE)
installSizeThreshold.convention(Long.MAX_VALUE)
}
}
Loading
Loading