• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.SignatureFileCache
20 import com.android.tools.metalava.cli.common.BaselineOptionsMixin
21 import com.android.tools.metalava.cli.common.CommonBaselineOptions
22 import com.android.tools.metalava.cli.common.ExecutionEnvironment
23 import com.android.tools.metalava.cli.common.PreviouslyReleasedApi
24 import com.android.tools.metalava.cli.common.allowStructuredOptionName
25 import com.android.tools.metalava.cli.common.existingFile
26 import com.android.tools.metalava.cli.common.map
27 import com.android.tools.metalava.model.Codebase
28 import com.android.tools.metalava.model.api.surface.ApiVariantType
29 import com.android.tools.metalava.model.visitors.ApiType
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 com.github.ajalt.clikt.parameters.options.unique
34 import java.io.File
35 
36 const val ARG_CHECK_COMPATIBILITY_API_RELEASED = "--check-compatibility:api:released"
37 const val ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED = "--check-compatibility:removed:released"
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 const val ARG_API_COMPAT_ANNOTATION = "--api-compat-annotation"
44 
45 /** The name of the group, can be used in help text to refer to the options in this group. */
46 const val COMPATIBILITY_CHECK_GROUP = "Compatibility Checks"
47 
48 class CompatibilityCheckOptions(
49     executionEnvironment: ExecutionEnvironment = ExecutionEnvironment(),
50     commonBaselineOptions: CommonBaselineOptions = CommonBaselineOptions(),
51 ) :
52     OptionGroup(
53         name = COMPATIBILITY_CHECK_GROUP,
54         help =
55             """
56                 Options controlling which, if any, compatibility checks are performed against a
57                 previously released API.
58             """
59                 .trimIndent(),
60     ) {
61 
62     private val checkReleasedApi: CheckRequest? by
63         option(
64                 ARG_CHECK_COMPATIBILITY_API_RELEASED,
65                 help =
66                     """
67                         Check compatibility of the previously released API.
68 
69                         When multiple files are provided any files that are a delta on another file
70                         must come after the other file, e.g. if `system` is a delta on `public` then
71                         `public` must come first, then `system`. Or, in other words, they must be
72                         provided in order from the narrowest API to the widest API.
73                     """
74                         .trimIndent(),
75             )
76             .existingFile()
77             .multiple()
78             .allowStructuredOptionName()
79             .map { CheckRequest.optionalCheckRequest(it, ApiType.PUBLIC_API) }
80 
81     private val checkReleasedRemoved: CheckRequest? by
82         option(
83                 ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED,
84                 help =
85                     """
86                         Check compatibility of the previously released but since removed APIs.
87 
88                         When multiple files are provided any files that are a delta on another file
89                         must come after the other file, e.g. if `system` is a delta on `public` then
90                         `public` must come first, then `system`. Or, in other words, they must be
91                         provided in order from the narrowest API to the widest API.
92                     """
93                         .trimIndent(),
94             )
95             .existingFile()
96             .multiple()
97             .allowStructuredOptionName()
98             .map { CheckRequest.optionalCheckRequest(it, ApiType.REMOVED) }
99 
100     internal val apiCompatAnnotations: Set<String> by
101         option(
102                 ARG_API_COMPAT_ANNOTATION,
103                 help =
104                     """
105                         Specify an annotation important for API compatibility.
106 
107                         Adding/removing this annotation will be considered an incompatible change.
108                         The fully qualified name of the annotation should be passed.
109                     """
110                         .trimIndent(),
111                 metavar = "<annotation>",
112             )
113             .multiple()
114             .unique()
115 
116     /**
117      * If set, metalava will show this error message when "check-compatibility:*:released" fails.
118      * (i.e. [ARG_CHECK_COMPATIBILITY_API_RELEASED] and [ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED])
119      */
120     internal val errorMessage: String? by
121         option(
122                 ARG_ERROR_MESSAGE_CHECK_COMPATIBILITY_RELEASED,
123                 help =
124                     """
125                         If set, this is output when errors are detected in
126                         $ARG_CHECK_COMPATIBILITY_API_RELEASED or
127                         $ARG_CHECK_COMPATIBILITY_REMOVED_RELEASED.
128                     """
129                         .trimIndent(),
130                 metavar = "<message>",
131             )
132             .allowStructuredOptionName()
133 
134     private val baselineOptionsMixin =
135         BaselineOptionsMixin(
136             containingGroup = this,
137             executionEnvironment,
138             baselineOptionName = ARG_BASELINE_CHECK_COMPATIBILITY_RELEASED,
139             updateBaselineOptionName = ARG_UPDATE_BASELINE_CHECK_COMPATIBILITY_RELEASED,
140             issueType = "compatibility",
141             description = "compatibility:released",
142             commonBaselineOptions = commonBaselineOptions,
143         )
144 
145     internal val baseline by baselineOptionsMixin::baseline
146 
147     /**
148      * Encapsulates information needed to perform a compatibility check of the current API being
149      * generated against a previously released API.
150      */
151     data class CheckRequest(
152         /**
153          * The previously released API with which the API being generated must be compatible.
154          *
155          * Each file is either a jar file (i.e. has an extension of `.jar`), or otherwise is a
156          * signature file. The latter's extension is not checked because while it usually has an
157          * extension of `.txt`, for legacy reasons Metalava will treat any file without a `,jar`
158          * extension as if it was a signature file.
159          */
160         val previouslyReleasedApi: PreviouslyReleasedApi,
161 
162         /** The part of the API to be checked. */
163         val apiType: ApiType,
164     ) {
165         /** The last signature file, if any, defining the previously released API. */
166         val lastSignatureFile by previouslyReleasedApi::lastSignatureFile
167 
168         companion object {
169             /** Create a [CheckRequest] if [files] is not empty, otherwise return `null`. */
170             internal fun optionalCheckRequest(files: List<File>, apiType: ApiType) =
171                 PreviouslyReleasedApi.optionalPreviouslyReleasedApi(
172                         checkCompatibilityOptionForApiType(apiType),
173                         files,
174                         apiVariantType =
175                             when (apiType) {
176                                 ApiType.REMOVED -> ApiVariantType.REMOVED
177                                 else -> ApiVariantType.CORE
178                             },
179                     )
180                     ?.let { previouslyReleasedApi -> CheckRequest(previouslyReleasedApi, apiType) }
181 
182             private fun checkCompatibilityOptionForApiType(apiType: ApiType) =
183                 "--check-compatibility:${apiType.flagName}:released"
184         }
185 
186         override fun toString(): String {
187             // This is only used when reporting progress.
188             return "${checkCompatibilityOptionForApiType(apiType)} $previouslyReleasedApi"
189         }
190     }
191 
192     /**
193      * The list of [CheckRequest] instances that need to be performed on the API being generated.
194      */
195     val compatibilityChecks by
196         lazy(LazyThreadSafetyMode.NONE) { listOfNotNull(checkReleasedApi, checkReleasedRemoved) }
197 
198     /**
199      * The optional Codebase corresponding to [compatibilityChecks].
200      *
201      * This is used to provide the previously released API needed for `--revert-annotation`.
202      */
203     fun previouslyReleasedCodebase(signatureFileCache: SignatureFileCache): Codebase? =
204         compatibilityChecks
205             .map { it.previouslyReleasedApi }
206             .reduceOrNull { p1, p2 -> p1.combine(p2) }
207             ?.load({ signatureFileCache.load(it) })
208 }
209