1 /*
<lambda>null2 * Copyright 2018 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.build.metalava
18
19 import androidx.build.checkapi.ApiLocation
20 import com.google.common.io.Files
21 import java.io.File
22 import org.gradle.api.DefaultTask
23 import org.gradle.api.GradleException
24 import org.gradle.api.logging.Logger
25 import org.gradle.api.provider.ListProperty
26 import org.gradle.api.provider.Property
27 import org.gradle.api.tasks.CacheableTask
28 import org.gradle.api.tasks.Input
29 import org.gradle.api.tasks.InputFiles
30 import org.gradle.api.tasks.Internal
31 import org.gradle.api.tasks.OutputFiles
32 import org.gradle.api.tasks.PathSensitive
33 import org.gradle.api.tasks.PathSensitivity
34 import org.gradle.api.tasks.TaskAction
35
36 /**
37 * Updates API signature text files. In practice, the values they will be updated to will match the
38 * APIs defined by the source code.
39 */
40 @CacheableTask
41 abstract class UpdateApiTask : DefaultTask() {
42
43 /** Text file from which API signatures will be read. */
44 @get:Input abstract val inputApiLocation: Property<ApiLocation>
45
46 /** Text files to which API signatures will be written. */
47 @get:Internal // outputs are declared in getTaskOutputs()
48 abstract val outputApiLocations: ListProperty<ApiLocation>
49
50 @InputFiles
51 @PathSensitive(PathSensitivity.RELATIVE)
52 fun getTaskInputs(): List<File> {
53 val inputApi = inputApiLocation.get()
54 return listOf(inputApi.publicApiFile, inputApi.restrictedApiFile)
55 }
56
57 @Suppress("unused")
58 @OutputFiles
59 fun getTaskOutputs(): List<File> {
60 return outputApiLocations.get().flatMap { outputApiLocation ->
61 listOf(
62 outputApiLocation.publicApiFile,
63 outputApiLocation.restrictedApiFile,
64 )
65 }
66 }
67
68 @TaskAction
69 fun exec() {
70 for (outputApi in outputApiLocations.get()) {
71 val inputApi = inputApiLocation.get()
72 copy(source = inputApi.publicApiFile, dest = outputApi.publicApiFile, logger = logger)
73 copy(
74 source = inputApi.restrictedApiFile,
75 dest = outputApi.restrictedApiFile,
76 logger = logger
77 )
78 }
79 }
80 }
81
copynull82 fun copy(source: File, dest: File, permitOverwriting: Boolean = true, logger: Logger? = null) {
83 if (!permitOverwriting) {
84 val sourceText =
85 if (source.exists()) {
86 source.readText()
87 } else {
88 ""
89 }
90 val overwriting = (dest.exists() && sourceText != dest.readText())
91 val changing = overwriting || (dest.exists() != source.exists())
92 if (changing) {
93 if (overwriting) {
94 val diff = summarizeDiff(source, dest, maxDiffLines + 1)
95 val diffMsg =
96 if (compareLineCount(diff, maxDiffLines) > 0) {
97 "Diff is greater than $maxDiffLines lines, use diff tool to compare.\n\n"
98 } else {
99 "Diff:\n$diff\n\n"
100 }
101 val message =
102 "Modifying the API definition for a previously released artifact " +
103 "having a final API version (version not ending in '-alpha') is not " +
104 "allowed.\n\n" +
105 "Previously declared definition is $dest\n" +
106 "Current generated definition is $source\n\n" +
107 diffMsg +
108 "Did you mean to increment the library version first?\n\n" +
109 "If you have a valid reason to override Semantic Versioning policy, see " +
110 "go/androidx/versioning#beta-api-change for information on obtaining " +
111 "approval."
112 throw GradleException(message)
113 }
114 }
115 }
116
117 if (source.exists()) {
118 Files.copy(source, dest)
119 logger?.lifecycle("Wrote ${dest.name}")
120 } else if (dest.exists()) {
121 dest.delete()
122 logger?.lifecycle("Deleted ${dest.name}")
123 }
124 }
125
126 /**
127 * Returns -1 if [text] has fewer than [count] newline characters, 0 if equal, and 1 if greater
128 * than.
129 */
compareLineCountnull130 fun compareLineCount(text: String, count: Int): Int {
131 var found = 0
132 var index = 0
133 while (found < count) {
134 index = text.indexOf('\n', index)
135 if (index < 0) {
136 break
137 }
138 found++
139 index++
140 }
141 return if (found < count) {
142 -1
143 } else if (found == count) {
144 0
145 } else {
146 1
147 }
148 }
149
150 /** Maximum number of diff lines to include in output. */
151 internal const val maxDiffLines = 8
152