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