1 /* <lambda>null2 * Copyright (C) 2024 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 com.android.tools.metalava.cli.compatibility 18 19 import com.android.tools.metalava.ApiType 20 import com.android.tools.metalava.SignatureFileCache 21 import com.android.tools.metalava.cli.common.BaselineOptionsMixin 22 import com.android.tools.metalava.cli.common.CommonBaselineOptions 23 import com.android.tools.metalava.cli.common.ExecutionEnvironment 24 import com.android.tools.metalava.cli.common.JarBasedApi 25 import com.android.tools.metalava.cli.common.PreviouslyReleasedApi 26 import com.android.tools.metalava.cli.common.allowStructuredOptionName 27 import com.android.tools.metalava.cli.common.existingFile 28 import com.android.tools.metalava.cli.common.map 29 import com.android.tools.metalava.model.Codebase 30 import com.github.ajalt.clikt.parameters.groups.OptionGroup 31 import com.github.ajalt.clikt.parameters.options.multiple 32 import com.github.ajalt.clikt.parameters.options.option 33 import java.io.File 34 35 const val ARG_CHECK_COMPATIBILITY_API_RELEASED = "--check-compatibility:api:released" 36 const val ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED = "--check-compatibility:removed:released" 37 const val ARG_CHECK_COMPATIBILITY_BASE_API = "--check-compatibility:base" 38 const val ARG_ERROR_MESSAGE_CHECK_COMPATIBILITY_RELEASED = "--error-message:compatibility:released" 39 40 const val ARG_BASELINE_CHECK_COMPATIBILITY_RELEASED = "--baseline:compatibility:released" 41 const val ARG_UPDATE_BASELINE_CHECK_COMPATIBILITY_RELEASED = 42 "--update-baseline:compatibility:released" 43 44 /** The name of the group, can be used in help text to refer to the options in this group. */ 45 const val COMPATIBILITY_CHECK_GROUP = "Compatibility Checks" 46 47 class CompatibilityCheckOptions( 48 executionEnvironment: ExecutionEnvironment = ExecutionEnvironment(), 49 commonBaselineOptions: CommonBaselineOptions = CommonBaselineOptions(), 50 ) : 51 OptionGroup( 52 name = COMPATIBILITY_CHECK_GROUP, 53 help = 54 """ 55 Options controlling which, if any, compatibility checks are performed against a 56 previously released API. 57 """ 58 .trimIndent(), 59 ) { 60 61 internal val baseApiForCompatCheck: File? by 62 option( 63 ARG_CHECK_COMPATIBILITY_BASE_API, 64 help = 65 """ 66 When performing a compat check, use the provided signature file as a base 67 api, which is treated as part of the API being checked. This allows us to 68 compute the full API surface from a partial API surface (e.g. the current 69 @SystemApi txt file), which allows us to recognize when an API is moved 70 from the partial API to the base API and avoid incorrectly flagging this 71 """ 72 .trimIndent(), 73 ) 74 .existingFile() 75 .allowStructuredOptionName() 76 77 private val checkReleasedApi: CheckRequest? by 78 option( 79 ARG_CHECK_COMPATIBILITY_API_RELEASED, 80 help = 81 """ 82 Check compatibility of the previously released API. 83 84 When multiple files are provided any files that are a delta on another file 85 must come after the other file, e.g. if `system` is a delta on `public` then 86 `public` must come first, then `system`. Or, in other words, they must be 87 provided in order from the narrowest API to the widest API. 88 """ 89 .trimIndent(), 90 ) 91 .existingFile() 92 .multiple() 93 .allowStructuredOptionName() 94 .map { CheckRequest.optionalCheckRequest(it, ApiType.PUBLIC_API) } 95 96 private val checkReleasedRemoved: CheckRequest? by 97 option( 98 ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED, 99 help = 100 """ 101 Check compatibility of the previously released but since removed APIs. 102 103 When multiple files are provided any files that are a delta on another file 104 must come after the other file, e.g. if `system` is a delta on `public` then 105 `public` must come first, then `system`. Or, in other words, they must be 106 provided in order from the narrowest API to the widest API. 107 """ 108 .trimIndent(), 109 ) 110 .existingFile() 111 .multiple() 112 .allowStructuredOptionName() 113 .map { CheckRequest.optionalCheckRequest(it, ApiType.REMOVED) } 114 115 /** 116 * If set, metalava will show this error message when "check-compatibility:*:released" fails. 117 * (i.e. [ARG_CHECK_COMPATIBILITY_API_RELEASED] and [ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED]) 118 */ 119 internal val errorMessage: String? by 120 option( 121 ARG_ERROR_MESSAGE_CHECK_COMPATIBILITY_RELEASED, 122 help = 123 """ 124 If set, this is output when errors are detected in 125 $ARG_CHECK_COMPATIBILITY_API_RELEASED or 126 $ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED. 127 """ 128 .trimIndent(), 129 metavar = "<message>", 130 ) 131 .allowStructuredOptionName() 132 133 private val baselineOptionsMixin = 134 BaselineOptionsMixin( 135 containingGroup = this, 136 executionEnvironment, 137 baselineOptionName = ARG_BASELINE_CHECK_COMPATIBILITY_RELEASED, 138 updateBaselineOptionName = ARG_UPDATE_BASELINE_CHECK_COMPATIBILITY_RELEASED, 139 issueType = "compatibility", 140 description = "compatibility:released", 141 commonBaselineOptions = commonBaselineOptions, 142 ) 143 144 internal val baseline by baselineOptionsMixin::baseline 145 146 /** 147 * Encapsulates information needed to perform a compatibility check of the current API being 148 * generated against a previously released API. 149 */ 150 data class CheckRequest( 151 /** 152 * The previously released API with which the API being generated must be compatible. 153 * 154 * Each file is either a jar file (i.e. has an extension of `.jar`), or otherwise is a 155 * signature file. The latter's extension is not checked because while it usually has an 156 * extension of `.txt`, for legacy reasons Metalava will treat any file without a `,jar` 157 * extension as if it was a signature file. 158 */ 159 val previouslyReleasedApi: PreviouslyReleasedApi, 160 161 /** The part of the API to be checked. */ 162 val apiType: ApiType, 163 ) { 164 /** The last signature file, if any, defining the previously released API. */ 165 val lastSignatureFile by previouslyReleasedApi::lastSignatureFile 166 167 companion object { 168 /** Create a [CheckRequest] if [files] is not empty, otherwise return `null`. */ 169 internal fun optionalCheckRequest(files: List<File>, apiType: ApiType) = 170 PreviouslyReleasedApi.optionalPreviouslyReleasedApi( 171 checkCompatibilityOptionForApiType(apiType), 172 files 173 ) 174 ?.let { previouslyReleasedApi -> 175 // It makes no sense to supply a jar file for the removed API because the 176 // removed API is only a tiny fraction and incomplete part of an API 177 // surface, so it could never be guaranteed to be able to compile into a jar 178 // file. 179 if (apiType == ApiType.REMOVED && previouslyReleasedApi is JarBasedApi) { 180 throw IllegalStateException( 181 "$ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED: Cannot specify jar files for removed API but found ${previouslyReleasedApi.file}" 182 ) 183 } 184 CheckRequest(previouslyReleasedApi, apiType) 185 } 186 187 private fun checkCompatibilityOptionForApiType(apiType: ApiType) = 188 "--check-compatibility:${apiType.flagName}:released" 189 } 190 191 override fun toString(): String { 192 // This is only used when reporting progress. 193 return "${checkCompatibilityOptionForApiType(apiType)} $previouslyReleasedApi" 194 } 195 } 196 197 /** 198 * The list of [CheckRequest] instances that need to be performed on the API being generated. 199 */ 200 val compatibilityChecks by 201 lazy(LazyThreadSafetyMode.NONE) { listOfNotNull(checkReleasedApi, checkReleasedRemoved) } 202 203 /** 204 * The list of [Codebase]s corresponding to [compatibilityChecks]. 205 * 206 * This is used to provide the previously released API needed for `--revert-annotation`. It does 207 * not support jar files. 208 */ 209 fun previouslyReleasedCodebases(signatureFileCache: SignatureFileCache): List<Codebase> = 210 compatibilityChecks.map { 211 it.previouslyReleasedApi.load( 212 { 213 throw IllegalStateException( 214 "Unexpected file $it: jar files do not work with --revert-annotation" 215 ) 216 }, 217 { signatureFileCache.load(it) } 218 ) 219 } 220 } 221