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