1 /*
<lambda>null2  * Copyright (C) 2023 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.signature
18 
19 import com.android.tools.metalava.OptionsDelegate
20 import com.android.tools.metalava.cli.common.MetalavaSubCommand
21 import com.android.tools.metalava.cli.common.existingFile
22 import com.android.tools.metalava.cli.common.stderr
23 import com.android.tools.metalava.model.text.FileFormat
24 import com.android.tools.metalava.model.text.FileFormat.Companion.parseHeader
25 import com.github.ajalt.clikt.parameters.arguments.argument
26 import com.github.ajalt.clikt.parameters.arguments.multiple
27 import com.github.ajalt.clikt.parameters.groups.provideDelegate
28 import java.io.File
29 import java.io.LineNumberReader
30 import java.nio.file.Files
31 import java.nio.file.StandardCopyOption
32 import kotlin.io.path.bufferedWriter
33 import kotlin.io.path.createTempFile
34 
35 class UpdateSignatureHeaderCommand :
36     MetalavaSubCommand(
37         help =
38             """
39                 Updates the header of signature files to a different format.
40 
41                 The purpose of this is, by working in conjunction with the $ARG_USE_SAME_FORMAT_AS
42                 option, to simplify the process for updating signature files from one version to the
43                 next. It assumes a number of things:
44 
45                 1. That API signature files are checked into some version control system and need to
46                 be updated to reflect changes to the API. If they are not then this is not needed.
47 
48                 2. That there is some integration with metalava in the build system which allows the
49                 automated updating of the checked in signature files, e.g. in the Android build it
50                 is `m update-api`.
51 
52                 3. The build uses the $ARG_USE_SAME_FORMAT_AS to pass the checked in API signature
53                 file so that its format will be used as the output for the file that the build
54                 generates to replace it.
55 
56                 If those assumptions are met then updating the format version of the API file (and
57                 its corresponding removed API file if needed) simply involves:
58 
59                 1. Running this command on the API file specifying the required format. That will
60                 update the header of the file but will not actually update its contents.
61 
62                 2. Running the normal build process to update the APIs, e.g. `m update-api`. That
63                 will read the now modified format from the API file and use it to generate the
64                 replacement file, including updating its contents which will then be copied over the
65                 API file completing the process.
66             """
67                 .trimIndent()
68     ) {
69 
70     private val formatOptions by SignatureFormatOptions(migratingAllowed = true)
71 
72     private val files by
73         argument(
74                 name = "<files>",
75                 help =
76                     """
77                         Signature files whose headers will be updated to the format specified by the
78                         $SIGNATURE_FORMAT_OUTPUT_GROUP options.
79                     """
80                         .trimIndent()
81             )
82             .existingFile()
83             .multiple(required = true)
84 
85     override fun run() {
86         // Make sure that none of the code called by this command accesses the global `options`
87         // property.
88         OptionsDelegate.disallowAccess()
89 
90         val outputFormat = formatOptions.fileFormat
91 
92         files.forEach { updateHeader(outputFormat, it) }
93     }
94 
95     private fun updateHeader(outputFormat: FileFormat, file: File) {
96         try {
97             LineNumberReader(file.reader()).use { reader ->
98                 // Read the format from the file. That will consume the header (and only the header)
99                 // from the reader so that it can be used to read the rest of the content.
100                 val currentFormat = parseHeader(file.toPath(), reader)
101 
102                 // If the format is not changing then do nothing.
103                 if (outputFormat == currentFormat) {
104                     return
105                 }
106 
107                 // Create a temporary file and write the updated contents into it.
108                 val temp = createTempFile("${file.name}-${outputFormat.version.name}")
109                 temp.bufferedWriter().use { writer ->
110                     // Write the new header.
111                     writer.write(outputFormat.header())
112 
113                     // Read the rest of the content from the original file (excluding the header)
114                     // and write that to the new file.
115                     do {
116                         val line = reader.readLine() ?: break
117                         writer.write(line)
118                         writer.write("\n")
119                     } while (true)
120                 }
121 
122                 // Move the new file over the original file.
123                 Files.move(
124                     temp,
125                     file.toPath(),
126                     StandardCopyOption.REPLACE_EXISTING,
127                     StandardCopyOption.ATOMIC_MOVE
128                 )
129             }
130         } catch (e: Exception) {
131             stderr.println("Could not update header for $file: ${e.message}")
132         }
133     }
134 }
135