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