Radha Nakade | 3556e79 | 2024-06-07 11:43:50 -0700 | [diff] [blame] | 1 | #!/usr/bin/env kotlin |
| 2 | |
| 3 | /* |
| 4 | * Copyright 2024 The Android Open Source Project |
| 5 | * |
| 6 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 7 | * you may not use this file except in compliance with the License. |
| 8 | * You may obtain a copy of the License at |
| 9 | * |
| 10 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | * |
| 12 | * Unless required by applicable law or agreed to in writing, software |
| 13 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | * See the License for the specific language governing permissions and |
| 16 | * limitations under the License. |
| 17 | */ |
| 18 | |
| 19 | /** |
| 20 | * To run .kts files, follow these steps: |
| 21 | * |
| 22 | * 1. Download and install the Kotlin compiler (kotlinc). There are several ways to do this; see |
| 23 | * https://kotlinlang.org/docs/command-line.html |
| 24 | * 2. Run the script from the command line: |
| 25 | * <path_to>/kotlinc -script <script_file>.kts <arguments> |
| 26 | */ |
| 27 | |
| 28 | @file:Repository("https://repo1.maven.org/maven2") |
| 29 | @file:DependsOn("com.google.code.gson:gson:2.11.0") |
| 30 | |
| 31 | import java.io.File |
| 32 | import java.util.Locale |
| 33 | import kotlin.system.exitProcess |
| 34 | import com.google.gson.Gson |
| 35 | |
| 36 | if (args.isEmpty() || !args.contains("-input")) { |
| 37 | println("Expected path to buildHealth Advice json file using -input.") |
| 38 | println("Provide output file path using -output. By default, output.csv will be generated " + |
| 39 | "in the current directory") |
| 40 | println("Usage ex: kotlinc -script buildHealthAdviceToCsv.main.kts -- -input " + |
| 41 | "path/to/build-health-report.json -output /path/to/output.csv") |
| 42 | exitProcess(1) |
| 43 | } |
| 44 | |
| 45 | val input = args[1] |
| 46 | if (!File(input).exists()) { |
| 47 | println("Could not find input files: $input") |
| 48 | exitProcess(1) |
| 49 | } |
| 50 | val inputJsonFile = File(input) |
| 51 | println("Parsing ${inputJsonFile.path}...") |
| 52 | |
| 53 | val outputFile = if(args.contains("-output")) { |
| 54 | args[3] |
| 55 | } else { |
| 56 | "output.csv" |
| 57 | } |
| 58 | |
| 59 | val csvOutputFile = File(outputFile) |
| 60 | if(csvOutputFile.exists()) { |
| 61 | csvOutputFile.delete() |
| 62 | } |
| 63 | |
| 64 | val csvData = StringBuilder() |
| 65 | val columnHeaders = listOf( |
| 66 | "ID", // leave blank |
| 67 | "Priority", |
| 68 | "Summary", |
| 69 | "HotlistIds", |
| 70 | "Assignee", // leave blank |
| 71 | "Status", |
| 72 | "ComponentPath", |
| 73 | "Reporter", |
| 74 | "FirstNote" |
| 75 | ) |
| 76 | csvData.append(columnHeaders.joinToString(",")) |
| 77 | csvData.append("\n") |
| 78 | |
| 79 | val gson = Gson() |
| 80 | val advice: Advice = gson.fromJson(inputJsonFile.readLines().first(), Advice::class.java) |
| 81 | |
| 82 | //list of projects we want to file issues for |
| 83 | //val androidxProjects = listOf("lint", "lint-checks", "buildSrc-tests", "androidx-demos", "stableaidl", "test") |
| 84 | //val flanProjects = listOf("activity", "fragment", "lifecycle", "navigation") |
| 85 | |
| 86 | var numProjects = 0 |
| 87 | advice.projectAdvice.forEach projectAdvice@{ projectAdvice -> |
| 88 | val projectPath = projectAdvice.projectPath |
| 89 | val title = "Please fix misconfigured dependencies for $projectPath" |
| 90 | |
| 91 | val project = projectPath.split(":")[1] |
| 92 | |
| 93 | // Uncomment the following section if you want to create bugs for specific projects |
| 94 | // if(project !in flanProjects) { |
| 95 | // return@projectAdvice |
| 96 | // } |
| 97 | |
Radha Nakade | b338289 | 2024-07-26 13:57:50 -0700 | [diff] [blame] | 98 | // Ignore advice for lint projects: b/350084892 |
| 99 | if (projectPath.contains("lint")) { |
| 100 | return@projectAdvice |
| 101 | } |
| 102 | |
Radha Nakade | 3556e79 | 2024-06-07 11:43:50 -0700 | [diff] [blame] | 103 | val description = StringBuilder() |
| 104 | description.appendLine( |
| 105 | "The dependency analysis gradle plugin found some dependencies that may have been " + |
| 106 | "misconfigured. Please fix the following dependencies: \n" |
| 107 | ) |
| 108 | |
| 109 | val unused = mutableSetOf<String>() |
| 110 | val transitive = mutableSetOf<String>() |
| 111 | val modified = mutableSetOf<String>() |
| 112 | projectAdvice.dependencyAdvice.forEach { dependencyAdvice -> |
| 113 | val fromConfiguration = dependencyAdvice.fromConfiguration |
| 114 | val toConfiguration = dependencyAdvice.toConfiguration |
| 115 | val coordinates = dependencyAdvice.coordinates |
| 116 | val resolvedVersion = coordinates.resolvedVersion |
| 117 | |
| 118 | val isCompileOnly = toConfiguration?.endsWith("compileOnly", ignoreCase = true) == true |
| 119 | val isModifyDependencyAdvice = fromConfiguration != null && toConfiguration != null |
| 120 | val isTransitiveDependencyAdvice = fromConfiguration == null && toConfiguration != null && !isCompileOnly |
| 121 | val isUnusedDependencyAdvice = fromConfiguration != null && toConfiguration == null |
| 122 | |
Radha Nakade | b338289 | 2024-07-26 13:57:50 -0700 | [diff] [blame] | 123 | // Ignore advice for androidx.profileinstaller:profileinstaller. |
| 124 | // It needs to remain implementation as that needs to be part of the manifest merger |
| 125 | // which is before runtime (b/355239547) |
| 126 | if(coordinates.identifier == "androidx.profileinstaller:profileinstaller") { |
| 127 | return@forEach |
| 128 | } |
| 129 | |
Radha Nakade | 3556e79 | 2024-06-07 11:43:50 -0700 | [diff] [blame] | 130 | var identifier = if(resolvedVersion == null) { |
| 131 | "'${coordinates.identifier}'" |
| 132 | } else { |
| 133 | "'${coordinates.identifier}:${coordinates.resolvedVersion}'" |
| 134 | } |
| 135 | if (coordinates.type == "project") { |
| 136 | identifier = "project($identifier)" |
| 137 | } |
| 138 | if (isModifyDependencyAdvice) { |
| 139 | modified.add("$toConfiguration($identifier) (was $fromConfiguration)") |
| 140 | } |
| 141 | if(isTransitiveDependencyAdvice) { |
| 142 | transitive.add("$toConfiguration($identifier)") |
| 143 | } |
| 144 | if(isUnusedDependencyAdvice) { |
| 145 | unused.add("$fromConfiguration($identifier)") |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | if(unused.isNotEmpty()) { |
| 150 | description.appendLine("Unused dependencies which should be removed:") |
| 151 | description.appendLine("```") |
| 152 | description.appendLine(unused.sorted().joinToString(separator = "\n")) |
| 153 | description.appendLine("```") |
| 154 | |
| 155 | } |
| 156 | if (transitive.isNotEmpty()) { |
| 157 | description.appendLine("These transitive dependencies can be declared directly:") |
| 158 | description.appendLine("```") |
| 159 | description.appendLine(transitive.sorted().joinToString(separator = "\n")) |
| 160 | description.appendLine("```") |
| 161 | } |
| 162 | if (modified.isNotEmpty()) { |
| 163 | description.appendLine( |
| 164 | "These dependencies can be modified to be as indicated. Please be careful " + |
| 165 | "while changing the type of dependencies since it can affect the consumers of " + |
| 166 | "this library. To learn more about the various dependency configurations, " + |
| 167 | "please visit: [dac]" + |
| 168 | "(https://developer.android.com/build/dependencies#dependency_configurations). " + |
| 169 | "Also check [Gradle's guide for dependency management]" + |
| 170 | "(https://docs.gradle.org/current/userguide/dependency_management.html).\n" |
| 171 | ) |
| 172 | description.appendLine("```") |
| 173 | description.appendLine(modified.sorted().joinToString(separator = "\n")) |
| 174 | description.appendLine("```") |
| 175 | } |
| 176 | |
| 177 | description.appendLine("To get more up-to-date project advice, please run:") |
| 178 | description.appendLine("```") |
| 179 | description.appendLine("./gradlew $projectPath:projectHealth") |
| 180 | description.appendLine("```") |
| 181 | val newColumns = listOf( |
| 182 | "NEW00000", // ID |
| 183 | "P2", // priority |
| 184 | title, |
| 185 | "=HYPERLINK(\"https://issuetracker.google.com/issues?q=status%3Aopen%20hotlistid%3A(5997499)\", \"Androidx misconfigured dependencies\")", |
| 186 | "", // Assignee: leave blank |
| 187 | "Assigned", // status |
| 188 | "Android > Android OS & Apps > Jetpack (androidx) > ${project.replaceFirstChar { |
| 189 | if (it.isLowerCase()) it.titlecase( |
| 190 | Locale.getDefault() |
| 191 | ) else it.toString() |
| 192 | }}", |
| 193 | "", // reporter: add your ldap here |
| 194 | description.toString() |
| 195 | ) |
| 196 | |
| 197 | if (projectAdvice.dependencyAdvice.isNotEmpty()) { |
| 198 | numProjects++ |
| 199 | csvData.append(newColumns.joinToString(",") { data -> |
| 200 | "\"${data.replace("\"", "\"\"")}\"" |
| 201 | }) |
| 202 | csvData.append("\n") |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | csvOutputFile.appendText(csvData.toString()) |
| 207 | println("Wrote CSV output to ${csvOutputFile.path} for $numProjects projects") |
| 208 | |
| 209 | data class Advice( |
| 210 | val projectAdvice: List<ProjectAdvice>, |
| 211 | ) |
| 212 | |
| 213 | data class ProjectAdvice( |
| 214 | val projectPath: String, |
| 215 | val dependencyAdvice: List<DependencyAdvice>, |
| 216 | val pluginAdvice: List<PluginAdvice>, |
| 217 | ) |
| 218 | |
| 219 | data class DependencyAdvice( |
| 220 | val coordinates: Coordinates, |
| 221 | val fromConfiguration: String?, |
| 222 | val toConfiguration: String? |
| 223 | ) |
| 224 | |
| 225 | data class Coordinates( |
| 226 | val type: String, |
| 227 | val identifier: String, |
| 228 | val resolvedVersion: String? |
| 229 | ) |
| 230 | |
| 231 | data class PluginAdvice( |
| 232 | val redundantPlugin: String, |
| 233 | val reason: String |
| 234 | ) |