Skip to content

Commit

Permalink
Implement ruler-cli to allow non-Gradle usage of Ruler
Browse files Browse the repository at this point in the history
  • Loading branch information
nathan3d committed Mar 21, 2023
1 parent 7c6dd72 commit c5fa13c
Show file tree
Hide file tree
Showing 63 changed files with 1,025 additions and 235 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
.cxx
local.properties
/kotlin-js-store
ruler-common/build
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ buildscript {
classpath(Dependencies.KOTLIN_REACT_FUNCTION_GRADLE_PLUGIN)
classpath(Dependencies.DETEKT_GRADLE_PLUGIN)
classpath(Dependencies.NEXUS_PUBLISH_GRADLE_PLUGIN)
classpath(Dependencies.SHADOW_GRADLE_PLUGIN)

if (!properties.containsKey("withoutSample")) {
classpath(Dependencies.RULER_GRADLE_PLUGIN)
Expand Down
4 changes: 4 additions & 0 deletions buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ object Dependencies {
const val KOTLIN_REACT_FUNCTION_GRADLE_PLUGIN = "gradle.plugin.com.bnorm.react:kotlin-react-function-gradle:${Versions.KOTLIN_REACT_FUNCTION}"
const val DETEKT_GRADLE_PLUGIN = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:${Versions.DETEKT_GRADLE_PLUGIN}"
const val NEXUS_PUBLISH_GRADLE_PLUGIN = "io.github.gradle-nexus.publish-plugin:io.github.gradle-nexus.publish-plugin.gradle.plugin:${Versions.NEXUS_PUBLISH_GRADLE_PLUGIN}"
const val SHADOW_GRADLE_PLUGIN = "gradle.plugin.com.github.johnrengelman:shadow:${Versions.SHADOW_GRADLE_PLUGIN}"

const val BUNDLETOOL = "com.android.tools.build:bundletool:${Versions.BUNDLETOOL}"
const val PROTOBUF_CORE = "com.google.protobuf:protobuf-java:${Versions.PROTOBUF}"
Expand All @@ -32,6 +33,7 @@ object Dependencies {
const val ANDROID_TOOLS_SDKLIB = "com.android.tools:sdklib:${Versions.ANDROID_TOOLS}"

const val APK_ANALYZER = "com.android.tools.apkparser:apkanalyzer:${Versions.ANDROID_TOOLS}"
const val CLIKT = "com.github.ajalt.clikt:clikt:${Versions.CLIKT}"
const val KOTLINX_SERIALIZATION_CORE = "org.jetbrains.kotlinx:kotlinx-serialization-core:${Versions.KOTLINX_SERIALIZATION}"
const val KOTLINX_SERIALIZATION_JSON = "org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.KOTLINX_SERIALIZATION}"
const val SNAKE_YAML = "org.yaml:snakeyaml:${Versions.SNAKE_YAML}"
Expand Down Expand Up @@ -63,12 +65,14 @@ object Dependencies {
const val KOTLIN_REACT_FUNCTION = "0.7.0" // https://mvnrepository.com/artifact/com.bnorm.react.kotlin-react-function/com.bnorm.react.kotlin-react-function.gradle.plugin
const val DETEKT_GRADLE_PLUGIN = "1.21.0" // https://mvnrepository.com/artifact/io.gitlab.arturbosch.detekt/detekt-gradle-plugin
const val NEXUS_PUBLISH_GRADLE_PLUGIN = "1.1.0" // https://mvnrepository.com/artifact/io.github.gradle-nexus.publish-plugin/io.github.gradle-nexus.publish-plugin.gradle.plugin
const val SHADOW_GRADLE_PLUGIN = "7.1.2" // https://plugins.gradle.org/plugin/com.github.johnrengelman.shadow

const val BUNDLETOOL = "1.11.0" // https://mvnrepository.com/artifact/com.android.tools.build/bundletool
const val PROTOBUF = "3.21.6" // https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java
const val DEXLIB = "2.5.2" // https://mvnrepository.com/artifact/org.smali/dexlib2

const val ANDROID_TOOLS = "30.0.4" // https://mvnrepository.com/artifact/com.android.tools/common?repo=google
const val CLIKT = "3.5.1" // https://mvnrepository.com/artifact/com.github.ajalt.clikt/clikt
const val KOTLINX_SERIALIZATION = "1.3.3" // https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-serialization-core
const val SNAKE_YAML = "1.32" // https://mvnrepository.com/artifact/org.yaml/snakeyaml

Expand Down
1 change: 1 addition & 0 deletions ruler-cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
49 changes: 49 additions & 0 deletions ruler-cli/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2023 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.
*/

plugins {
id("application")
id("org.jetbrains.kotlin.jvm")
id("org.jetbrains.kotlin.plugin.serialization")
id("io.gitlab.arturbosch.detekt")
id("com.github.johnrengelman.shadow")
}

java {
withSourcesJar()

toolchain.languageVersion.set(JavaLanguageVersion.of(11))
}

dependencies {
implementation(project(":ruler-models"))
implementation(project(":ruler-common"))
implementation(Dependencies.CLIKT)
implementation(Dependencies.KOTLINX_SERIALIZATION_JSON)

testRuntimeOnly(Dependencies.JUNIT_ENGINE)
testImplementation(Dependencies.JUNIT_API)
testImplementation(Dependencies.JUNIT_PARAMS)
testImplementation(Dependencies.GOOGLE_TRUTH)
}

application {
mainClass.set("com.spotify.ruler.cli.RulerCliKt")
}

tasks.withType<Test> {
useJUnitPlatform()
}
133 changes: 133 additions & 0 deletions ruler-cli/src/main/java/com/spotify/ruler/cli/RulerCli.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Copyright 2023 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.
*/
@file:OptIn(ExperimentalSerializationApi::class)

package com.spotify.ruler.cli

import com.github.ajalt.clikt.core.CliktCommand
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.spotify.ruler.common.BaseRulerTask
import com.spotify.ruler.common.FEATURE_NAME
import com.spotify.ruler.common.dependency.ArtifactResult
import com.spotify.ruler.common.dependency.DependencyComponent
import com.spotify.ruler.common.dependency.DependencyEntry
import com.spotify.ruler.common.dependency.DependencySanitizer
import com.spotify.ruler.common.dependency.JarArtifactParser
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 kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import java.io.File

class RulerCli : CliktCommand(), BaseRulerTask {
private val dependencyMap by option().file().required()
private val rulerConfigJson by option().file().required()
private val apkFile by option().file().required()
private val reportDir by option().file(canBeDir = true).required()
private val mappingFile: File? by option().file()
private val resourceMappingFile: File? by option().file()

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

override fun provideMappingFile() = mappingFile

override fun provideResourceMappingFile(): File? = resourceMappingFile

override fun rulerConfig(): RulerConfig = config

private val config: RulerConfig by lazy {
val json = Json.decodeFromStream<JsonRulerConfig>(rulerConfigJson.inputStream())
RulerConfig(
projectPath = json.projectPath,
apkFilesMap = mapOf(FEATURE_NAME to listOf(apkFile)),
reportDir = reportDir,
ownershipFile = json.ownershipFile?.let { File(it) },
appInfo = json.appInfo,
deviceSpec = json.deviceSpec,
defaultOwner = json.defaultOwner,
omitFileBreakdown = json.omitFileBreakdown
)
}

private val dependencies: Map<String, List<DependencyComponent>> by lazy {
val json = Json.decodeFromStream<ModuleMap>(dependencyMap.inputStream())
val jarArtifactParser = JarArtifactParser()
val jarDependencies = json.jars.distinctBy {
it.jar
}.flatMap {
jarArtifactParser.parseFile(
ArtifactResult.JarArtifact(File(it.jar), it.module)
)
}

val assets = json.assets.map {
DependencyEntry.Default(it.filename, it.module)
}

val resources = json.resources.distinctBy { "${it.module}:${it.filename}" }.map {
DependencyEntry.Default(it.filename, it.module)
}

val entries = jarDependencies + assets + resources

val classNameSanitizer = ClassNameSanitizer(provideMappingFile())
val dependencySanitizer = DependencySanitizer(classNameSanitizer)
dependencySanitizer.sanitize(entries)
}

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

override fun run() {
super.run()
}
}

@Serializable
data class JsonRulerConfig(
val projectPath: String,
val ownershipFile: String? = null,
val appInfo: AppInfo,
val deviceSpec: DeviceSpec? = null,
val defaultOwner: String,
val omitFileBreakdown: Boolean
)

@Serializable
data class ModuleMap(
val assets: List<Asset>,
val jars: List<Jar>,
val resources: List<Asset>
)

@Serializable
data class Asset(
val filename: String,
val module: String
)

@Serializable
data class Jar(
val jar: String,
val module: String
)

fun main(args: Array<String>) = RulerCli().main(args)
89 changes: 89 additions & 0 deletions ruler-cli/src/test/java/com/spotify/ruler/cli/RulerCliTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.spotify.ruler.cli

import com.google.common.truth.Truth
import com.spotify.ruler.models.*
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import java.nio.file.Paths

class RulerCliTest {

private val dependencyMap = Paths.get("src", "test", "resources", "dependencyMap.json").toFile()
private val rulerConfig = Paths.get("src", "test", "resources", "rulerConfig.json").toFile()
private val testApk = Paths.get("src", "test", "resources", "test.apk").toFile()

private val proguardFile = Paths.get("src", "test", "resources", "test_proguard.map").toFile()

private val jsonResult = Paths.get("src", "test", "resources", "report.json").toFile()
private val htmlResult = Paths.get("src", "test", "resources", "report.html").toFile()

@Test
fun `Test all required cli arguments are passing`() {
RulerCli().parse(
listOf(
"--dependency-map", dependencyMap.path,
"--ruler-config-json", rulerConfig.path,
"--apk-file", testApk.path,
"--report-dir", "src/test/resources/"
)
)
val appReport = Json.decodeFromStream<AppReport>(jsonResult.inputStream())
Truth.assertThat(appReport.name).isEqualTo("com.ruler.example-bazel")
Truth.assertThat(appReport.version).isEqualTo("1.0.0")
Truth.assertThat(appReport.downloadSize).isEqualTo(14013)
Truth.assertThat(appReport.installSize).isEqualTo(14591)
Truth.assertThat(appReport.components.size).isEqualTo(1)

val appComponent = appReport.components[0]
Truth.assertThat(appComponent.name).isEqualTo("RulerTest")
Truth.assertThat(appComponent.type.toString()).isEqualTo("INTERNAL")
Truth.assertThat(appComponent.downloadSize).isEqualTo(14013)
Truth.assertThat(appComponent.installSize).isEqualTo(14591)
Truth.assertThat(appComponent.files?.size).isEqualTo(16)

// Test ownership file properly attributes owners
val attributedList = appComponent.files?.filter { it.owner == "ruler-test-team" }
Truth.assertThat(attributedList).containsExactly(
AppFile(
"com.spotify.ruler.sample.MainActivity",
type = FileType.CLASS,
downloadSize = 468,
installSize = 468,
owner = "ruler-test-team",
),
AppFile(
"/res/layout/activity_main.xml",
type = FileType.RESOURCE,
downloadSize = 257,
installSize = 257,
owner = "ruler-test-team",
resourceType = ResourceType.LAYOUT
)
)
}

@Test
fun `Test optional cli arguments are passing`() {
val cli = RulerCli()
cli.parse(
listOf(
"--dependency-map", dependencyMap.path,
"--ruler-config-json", rulerConfig.path,
"--apk-file", testApk.path,
"--report-dir", "src/test/resources/",
"--mapping-file", proguardFile.path
)
)
Truth.assertThat(cli.provideMappingFile()).isNotNull()
Truth.assertThat(cli.provideMappingFile()?.path).isEqualTo(proguardFile.path)
}

// Clean reports after each test
@AfterEach
fun cleanReports() {
jsonResult.delete()
htmlResult.delete()
}
}
20 changes: 20 additions & 0 deletions ruler-cli/src/test/resources/dependencyMap.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"assets": [
{
"filename": "/assets/test_asset.txt",
"module": "app"
}
],
"jars": [
{
"jar": "src/test/resources/test_jar.jar",
"module": "app"
}
],
"resources": [
{
"filename": "/drawable/activity_main.xml",
"module": "app"
}
]
}
11 changes: 11 additions & 0 deletions ruler-cli/src/test/resources/rulerConfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"appInfo": {
"applicationId": "com.ruler.example-bazel",
"variantName": "release",
"versionName": "1.0.0"
},
"defaultOwner": "main_activity",
"omitFileBreakdown": false,
"ownershipFile": "src/test/resources/test_ownership.yaml",
"projectPath": "RulerTest"
}
Binary file added ruler-cli/src/test/resources/test.apk
Binary file not shown.
Binary file added ruler-cli/src/test/resources/test_jar.jar
Binary file not shown.
5 changes: 5 additions & 0 deletions ruler-cli/src/test/resources/test_ownership.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- identifier: com.spotify.ruler.sample.MainActivity
owner: ruler-test-team

- identifier: /res/layout/activity_main.xml
owner: ruler-test-team
10 changes: 10 additions & 0 deletions ruler-cli/src/test/resources/test_proguard.map
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
com.spotify.ruler.sample.app.MainActivity -> com.spotify.ruler.sample.app.MainActivity:
void <init>() -> <init>
void onCreate(android.os.Bundle) -> onCreate
com.spotify.ruler.sample.lib.ClassToObfuscate -> com.spotify.ruler.sample.lib.a:
com.spotify.ruler.sample.lib.ClassToObfuscate INSTANCE -> a
void <clinit>() -> <clinit>
void <init>() -> <init>
com.spotify.ruler.sample.lib.LibActivity -> com.spotify.ruler.sample.lib.LibActivity:
void <init>() -> <init>
void onCreate(android.os.Bundle) -> onCreate
1 change: 1 addition & 0 deletions ruler-common/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
Loading

0 comments on commit c5fa13c

Please sign in to comment.