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