1 /*
<lambda>null2 * Copyright 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.baselineprofiles
18
19 import com.github.ajalt.clikt.core.CliktCommand
20 import com.github.ajalt.clikt.parameters.options.default
21 import com.github.ajalt.clikt.parameters.options.option
22 import com.github.ajalt.clikt.parameters.options.required
23 import java.io.File
24
25 /**
26 * A Utility that can split a monolithic baseline profiles to its constituent modules.
27 *
28 * This command helps split the large baseline profile produced via the `BaselineProfileRule`,
29 * to its constituent library modules by scanning the `checkoutPath` for source files, and matching
30 * the rules in the baseline profiles, to the packages discovered.
31 */
32 class SplitBaselineProfiles : CliktCommand() {
33
34 private val profilePath by option(
35 "-p",
36 "--profilePath",
37 help = "The baseline profile path"
38 ).required()
39
40 private val checkoutPath by option(
41 "-c",
42 "--checkoutPath",
43 help = "The checkout path"
44 ).required()
45
46 private val outputName by option(
47 "-o",
48 "--outputName",
49 help = "The name of the output file"
50 ).default("output-prof.txt")
51
52 override fun run() {
53 val profileFile = File(profilePath).requiredExists()
54 val checkoutFile = File(checkoutPath).requiredExists()
55 val outputFile = File(profileFile.parentFile, outputName)
56 val index = buildIndex(checkoutFile)
57 val contents = profileFile.readLines()
58 val groupedProfiles = contents.groupBy { line ->
59 val matchResult = CLASS_PATTERN.find(line)
60 val groupBy = if (matchResult != null) {
61 // The simplified class name
62 // Looks something like androidx/Foo/Bar
63 val name = matchResult.groupValues[2]
64 index[name] ?: name.split("/").dropLast(1).joinToString(separator = "/")
65 } else {
66 null
67 }
68 groupBy ?: "Unknown"
69 }
70 println("Writing to ${outputFile.absolutePath}")
71 writeOutputProfile(groupedProfiles, outputFile)
72 println("All Done.")
73 }
74
75 private fun buildIndex(checkoutFile: File): Map<String, String> {
76 val sourceFiles = mutableListOf<File>()
77 findSourceFiles(listOf(checkoutFile), sourceFiles)
78 println("Found ${sourceFiles.size} files.")
79 return buildIndexFromFiles(sourceFiles)
80 }
81
82 private fun buildIndexFromFiles(sourceFiles: List<File>): Map<String, String> {
83 val grouped = sourceFiles.groupBy { file ->
84 var currentFile = file
85 var group: String? = null
86 while (group == null) {
87 // Group by path to src/main because that is where the baseline-prof.txt
88 // Needs to end up in.
89 if (currentFile.isDirectory && currentFile.name == "main") {
90 group = currentFile.absolutePath
91 } else {
92 currentFile = currentFile.parentFile
93 }
94 }
95 group
96 }
97 // Invert
98 val inverted = mutableMapOf<String, String>()
99 grouped.forEach { (pathToMain, entries) ->
100 entries.forEach { file ->
101 val matchResult = PATH_PATTERN.find(file.absolutePath)
102 if (matchResult != null) {
103 val packagePath = matchResult.groupValues[3]
104 inverted[packagePath] = pathToMain
105 }
106 }
107 }
108 return inverted
109 }
110
111 private fun findSourceFiles(paths: List<File>, sourceFiles: MutableList<File>) {
112 if (paths.isEmpty()) {
113 return
114 }
115 val nextPaths = mutableListOf<File>()
116 for (path in paths) {
117 val files = path.listFiles()
118 files?.forEach { file ->
119 if (file.isFile) {
120 val name = file.name
121 // Only look at source files with a "src/main/" prefix
122 // They are gradle modules we might be interested in.
123 if ((name.endsWith("java") || name.endsWith("kt")) &&
124 file.absolutePath.contains("src/main")
125 ) {
126 sourceFiles += file
127 }
128 } else if (file.isDirectory) {
129 nextPaths += file
130 }
131 }
132 }
133 return findSourceFiles(nextPaths, sourceFiles)
134 }
135
136 private fun writeOutputProfile(groupedProfiles: Map<String, List<String>>, outputFile: File) {
137 val writer = outputFile.printWriter()
138 writer.use {
139 groupedProfiles.forEach { entry ->
140 val groupName = entry.key
141 val profileEntries = entry.value
142 writer.write("# $groupName\n\n")
143 profileEntries.forEach { line ->
144 writer.write("$line\n")
145 }
146 writer.write("\n")
147 }
148 }
149 }
150
151 companion object {
152 // A class file in the ART HRF format.
153 // We don't care about static inner classes, or methods to locate them in the source tree.
154 val CLASS_PATTERN = Regex("""(H|S|P)?L(\w+/[^$;]*)""")
155
156 // Identifies a Java | Kotlin Source folder from a checkout.
157 val PATH_PATTERN = Regex("""(.*)/src/main/(java|kotlin)/(.*)(\.(java|kt)?)""")
158
159 internal fun File.requiredExists(): File {
160 require(exists()) {
161 "$absolutePath does not exist."
162 }
163 return this
164 }
165 }
166 }
167
mainnull168 fun main(args: Array<String>) {
169 SplitBaselineProfiles().main(args)
170 }
171