1 /* <lambda>null2 * Copyright 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 androidx.build.metalava 18 19 import androidx.build.checkapi.SourceSetInputs 20 import com.google.common.annotations.VisibleForTesting 21 import java.io.File 22 import java.io.Writer 23 import org.dom4j.DocumentHelper 24 import org.dom4j.Element 25 import org.dom4j.io.OutputFormat 26 import org.dom4j.io.XMLWriter 27 import org.gradle.api.file.FileCollection 28 29 internal object ProjectXml { 30 /** 31 * Generates an XML file representing the structure of a KMP project, to be used by metalava. 32 * 33 * For more information see go/metalavatask-kmp-spec. 34 */ 35 fun create( 36 sourceSets: List<SourceSetInputs>, 37 bootClasspath: Collection<File>, 38 compiledSourceJar: File, 39 outputFile: File, 40 ) { 41 val sourceSetElements = 42 sourceSets.map { sourceSet -> 43 createSourceSetElement( 44 sourceSet.sourceSetName, 45 sourceSet.dependsOnSourceSets, 46 sourceFiles(sourceSet.sourcePaths), 47 (sourceSet.dependencyClasspath + bootClasspath), 48 compiledSourceJar, 49 ) 50 } 51 val projectElement = createProjectElement(sourceSetElements) 52 writeXml(projectElement, outputFile.writer()) 53 } 54 55 /** Writes the [element] as XML to the [writer] and closes the stream. */ 56 @VisibleForTesting 57 fun writeXml(element: Element, writer: Writer) { 58 val document = DocumentHelper.createDocument(element) 59 XMLWriter(writer, OutputFormat(/* indent= */ " ", /* newlines= */ true)).apply { 60 write(document) 61 close() 62 } 63 } 64 65 /** Constructs the XML [Element] for the project. */ 66 @VisibleForTesting 67 fun createProjectElement(sourceSets: List<Element>): Element { 68 val projectElement = DocumentHelper.createElement("project") 69 70 // Setting "." for the root dir is equivalent to using the project directory path. 71 val rootDirElement = DocumentHelper.createElement("root") 72 rootDirElement.addAttribute("dir", ".") 73 projectElement.add(rootDirElement) 74 75 for (sourceSet in sourceSets) { 76 projectElement.add(sourceSet) 77 } 78 79 return projectElement 80 } 81 82 /** Constructs the XML [Element] representing one source set. */ 83 @VisibleForTesting 84 fun createSourceSetElement( 85 sourceSetName: String, 86 dependsOnSourceSets: Collection<String>, 87 sourceFiles: Collection<File>, 88 allDependencies: Collection<File>, 89 compiledSourceJar: File, 90 ): Element { 91 val moduleElement = DocumentHelper.createElement("module") 92 moduleElement.addAttribute("name", sourceSetName) 93 // TODO(b/322156458): This should not be set to true for common, but it is necessary to 94 // allow annotations from androidx.annotation to be resolved in other projects. 95 moduleElement.addAttribute("android", "true") 96 97 for (dependsOn in dependsOnSourceSets) { 98 val depElement = DocumentHelper.createElement("dep") 99 depElement.addAttribute("module", dependsOn) 100 depElement.addAttribute("kind", "dependsOn") 101 moduleElement.add(depElement) 102 } 103 104 for (sourceFile in sourceFiles) { 105 val srcElement = DocumentHelper.createElement("src") 106 srcElement.addAttribute("file", sourceFile.absolutePath) 107 moduleElement.add(srcElement) 108 } 109 110 for (dependency in allDependencies) { 111 val (elementType, fileType) = 112 when (dependency.extension) { 113 "jar" -> "classpath" to "jar" 114 "klib" -> "klib" to "file" 115 "aar" -> "classpath" to "aar" 116 "" -> "classpath" to "dir" 117 else -> continue 118 } 119 120 val dependencyElement = DocumentHelper.createElement(elementType) 121 dependencyElement.addAttribute(fileType, dependency.absolutePath) 122 moduleElement.add(dependencyElement) 123 } 124 125 // Adding the compiled sources of this project fixes issues where annotations on some 126 // elements aren't registered by metalava (e.g. in :ink:ink-rendering). 127 val jarElement = DocumentHelper.createElement("src") 128 jarElement.addAttribute("jar", compiledSourceJar.absolutePath) 129 moduleElement.add(jarElement) 130 131 return moduleElement 132 } 133 134 /** Lists all of the files from [sources]. */ 135 private fun sourceFiles(sources: FileCollection): List<File> { 136 return sources.files.flatMap { gatherFiles(it) } 137 } 138 139 /** 140 * If [file] is a normal file, returns a list containing [file]. 141 * 142 * If [file] is a directory, returns a list of all normal files recursively contained in the 143 * directory. 144 * 145 * Otherwise, returns an empty list. 146 */ 147 private fun gatherFiles(file: File): List<File> { 148 return if (file.isFile) { 149 listOf(file) 150 } else if (file.isDirectory) { 151 file.listFiles()?.flatMap { gatherFiles(it) } ?: emptyList() 152 } else { 153 emptyList() 154 } 155 } 156 } 157