Merge "Clean up CLI integration project" into main
diff --git a/integration-tests/cli/build.gradle.kts b/integration-tests/cli/build.gradle.kts
index 8d93470..98f35fd 100644
--- a/integration-tests/cli/build.gradle.kts
+++ b/integration-tests/cli/build.gradle.kts
@@ -16,84 +16,174 @@
 
 // This project confirms that Dokka can be invoked as a .jar from the command line
 
-val generatedDir = project.file("${buildDir}/docs")
-
-configurations.create("runnerJar")
-
-dependencies.add(
-    "runnerJar",
-    project.dependencies.project(
-        mapOf(
-            "path" to ":",
-            "configuration" to "shadow"
+val runnerJar = configurations.create("runnerJar") {
+    dependencies.add(
+        project.dependencies.project(
+            mapOf(
+                "path" to ":",
+                "configuration" to "shadow"
+            )
         )
     )
-)
-
-tasks.register<Copy>("generateConfig") {
-    from("src/test/resources")
-    into("${project.buildDir}/resources")
-    filter { text ->
-        // Dackka requires absolute paths, so expand "$projectDir" into its value
-        text.replace("\$projectDir", "$projectDir")
-    }
 }
 
-tasks.register<JavaExec>("run") {
-    description = "Ensures that dackka can be invoked as a .jar from the command line"
+class GlobalDocsLink(
+    @get:Input
+    val url: String,
+    @get:[InputFile PathSensitive(PathSensitivity.NONE)]
+    val packageListUrl: RegularFile
+)
 
-    dependsOn("generateConfig", project.configurations.getByName("runnerJar"))
-    classpath = files({
-        project.configurations.getByName("runnerJar").resolvedConfiguration.files
-    })
-    outputs.dir(generatedDir)
+@CacheableTask
+abstract class DackkaRunner : DefaultTask() {
+    @get:Inject
+    abstract val execOperations: ExecOperations
+    @get:Classpath
+    abstract val dackkaClasspath: ConfigurableFileCollection
+    @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)]
+    abstract val sourceDirectories: ConfigurableFileCollection
+    @get:[InputDirectory PathSensitive(PathSensitivity.RELATIVE)]
+    abstract val samplesDirectory: DirectoryProperty
+    @get:[InputFile PathSensitive(PathSensitivity.NONE)]
+    abstract val include: RegularFileProperty
+    @get:Nested
+    abstract val globalDocsLinks: ListProperty<GlobalDocsLink>
 
-    args = listOf(
-        "${project.buildDir}/resources/config.json",
-        "-loggingLevel", "WARN"
-    )
+    @get:OutputFile
+    abstract val config: RegularFileProperty
+    @get:OutputDirectory
+    abstract val outputDirectory: DirectoryProperty
+    @get:Internal
+    abstract val projectDirectory: DirectoryProperty
 
-    doFirst {
-        generatedDir.deleteRecursively()
-    }
+    @TaskAction
+    fun run() {
+        generateConfig()
+        outputDirectory.get().asFile.deleteRecursively()
 
-    // Track all warning lines to write to a file sorted after dackka finishes
-    val loggedLines = mutableListOf<String>()
-    val absoluteSourcePath = projectDir.absolutePath
-    logging.addStandardOutputListener {
-        if (it.isNotBlank()) {
-            loggedLines.add(it.toString().replace(absoluteSourcePath, "\$SRC_DIR"))
+        // Track all warning lines to write to a file sorted after dackka finishes
+        val loggedLines = mutableListOf<String>()
+
+        execOperations.javaexec {
+            classpath = dackkaClasspath
+            args = listOf(
+                config.get().asFile.absolutePath,
+                "-loggingLevel", "WARN"
+            )
+            logging.addStandardOutputListener {
+                if (it.isNotBlank()) {
+                    loggedLines.add(
+                        it.toString().replace(
+                            projectDirectory.get().asFile.absolutePath,
+                            "\$SRC_DIR"
+                        )
+                    )
+                }
+            }
         }
-    }
-    doLast {
+
         if (loggedLines.isNotEmpty()) {
-            val loggingFile = File(generatedDir, "logging.txt")
+            val loggingFile = File(outputDirectory.get().asFile, "logging.txt")
             loggingFile.writeText(loggedLines.sorted().joinToString("\n"))
         }
     }
+
+    private fun generateConfig() {
+        val configContents = """
+            {
+              "outputDir": "${outputDirectory.get().asFile.absolutePath}",
+              "offlineMode": "true",
+              "sourceSets": [
+                {
+                  "moduleDisplayName": "Sample",
+                  "externalDocumentationLinks": [
+${globalDocsLinks.get().joinToString(separator = ",\n") {
+"""                        { "url": "${it.url}", "packageListUrl": "file://${it.packageListUrl.asFile.absolutePath}" }"""
+}}
+                  ],
+                  "sourceSetID": {
+                    "scopeId": "sample",
+                    "sourceSetName": "main"
+                  },
+                  "sourceRoots": [${sourceDirectories.joinToString(separator = ", ") { "\"${it.absolutePath}\"" }}],
+                  "samples": ["${samplesDirectory.get().asFile.absolutePath}"],
+                  "include": ["${include.get().asFile.absolutePath}"],
+                  "documentedVisibilities": ["PUBLIC", "PROTECTED"]
+                }
+              ],
+                "pluginsConfiguration": [
+                  {
+                    "fqPluginName": "com.google.devsite.DevsitePlugin",
+                    "serializationFormat": "JSON",
+                    "values": "{ \"projectPath\": \"androidx\", \"excludedPackages\": [ \".*excluded.*\" ], \"javaDocsPath\": \"\", \"kotlinDocsPath\": \"kotlin\", \"annotationsNotToDisplay\": [ \"java.lang.Override\", \"kotlin.ParameterName\" ] }"
+                  }
+                ]
+            }
+        """.trimIndent()
+        config.get().asFile.writeText(configContents)
+    }
 }
 
-tasks.register("verifyRun") {
-    dependsOn("run")
-    inputs.dir(generatedDir)
-    outputs.file(File(temporaryDir, "fake-output-for-up-to-date-checks"))
+val runTask = tasks.register<DackkaRunner>("run") {
+    description = "Ensures that dackka can be invoked as a .jar from the command line"
+    dackkaClasspath.from(runnerJar)
+    sourceDirectories.from(
+        layout.projectDirectory.dir("src/testData/androidx/paging/source"),
+        layout.projectDirectory.dir("src/testData/androidx/paging/excluded"),
+    )
+    samplesDirectory.set(
+        layout.projectDirectory.dir("src/testData/androidx/paging/samples")
+    )
+    include.set(layout.projectDirectory.file("src/testData/androidx/paging/metadata.md"))
+    globalDocsLinks.addAll(
+        GlobalDocsLink(
+            url = "https://kotlinlang.org/api/latest/jvm/stdlib/",
+            rootProject.layout.projectDirectory.file("testData/package-lists/kotlin/package-list")
+        ),
+        GlobalDocsLink(
+            url = "https://developer.android.com/reference",
+            rootProject.layout.projectDirectory.file("testData/package-lists/android/package-list")
+        ),
+        GlobalDocsLink(
+            url = "https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core",
+            rootProject.layout.projectDirectory.file("testData/package-lists/coroutines/package-list")
+        ),
+        GlobalDocsLink(
+            url = "https://guava.dev/releases/18.0/api/docs/package-list",
+            rootProject.layout.projectDirectory.file("testData/package-lists/guava/package-list")
+        )
+    )
 
-    doLast {
-        val expectedPaths = listOf(
+    outputDirectory.set(project.layout.buildDirectory.dir("docs"))
+    config.set(project.layout.buildDirectory.file("resources/config.json"))
+    projectDirectory.set(project.layout.projectDirectory)
+}
+
+@CacheableTask
+abstract class VerifyRun : DefaultTask() {
+    @get:[InputDirectory PathSensitive(PathSensitivity.RELATIVE)]
+    abstract val directoryToValidate: DirectoryProperty
+    @TaskAction
+    fun verify() {
+        listOf(
             "reference/androidx/index.html",
             "reference/androidx/classes.html",
             "reference/androidx/packages.html"
-        )
-
-        for (relativePath in expectedPaths) {
-            val path = "${generatedDir}/$relativePath"
-            if (!file(path).exists()) {
-                throw GradleException("Failed to create $path")
+        ).map {
+            File(directoryToValidate.get().asFile, it)
+        }.forEach {
+            if (!it.exists()) {
+                throw GradleException("Failed to create $it")
             }
         }
     }
 }
 
+tasks.register<VerifyRun>("verifyRun") {
+    directoryToValidate.set(runTask.flatMap { it.outputDirectory })
+    outputs.file(File(temporaryDir, "fake-output-for-up-to-date-checks"))
+}
+
 tasks.register("test") {
     dependsOn("verifyRun")
 }
diff --git a/integration-tests/cli/src/test/resources/config.json b/integration-tests/cli/src/test/resources/config.json
deleted file mode 100644
index 4c6ae91..0000000
--- a/integration-tests/cli/src/test/resources/config.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
-  "outputDir": "build/docs",
-  "offlineMode": "true",
-  "sourceSets": [
-    {
-      "moduleDisplayName": "Sample",
-      "externalDocumentationLinks": [
-        {
-          "url": "https://kotlinlang.org/api/latest/jvm/stdlib/",
-          "packageListUrl": "file://$projectDir/../../testData/package-lists/kotlin/package-list"
-        },
-        {
-          "url": "https://developer.android.com/reference",
-          "packageListUrl": "file://$projectDir/../../testData/package-lists/android/package-list"
-        },
-        {
-          "url": "https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core",
-          "packageListUrl": "file://$projectDir/../../testData/package-lists/coroutines/package-list"
-        },
-        {
-          "url": "https://guava.dev/releases/18.0/api/docs/package-list",
-          "packageListUrl": "file://$projectDir/../../testData/package-lists/guava/package-list"
-        }
-      ],
-      "sourceSetID": {
-        "scopeId": "sample",
-        "sourceSetName": "main"
-      },
-      "sourceRoots": ["$projectDir/src/testData/androidx/paging/source", "$projectDir/src/testData/androidx/paging/excluded"],
-      "samples": ["$projectDir/src/testData/androidx/paging/samples"],
-      "include": ["$projectDir/src/testData/androidx/paging/metadata.md"],
-      "documentedVisibilities": ["PUBLIC", "PROTECTED"]
-    }
-  ],
-  "pluginsConfiguration": [
-    {
-      "fqPluginName": "com.google.devsite.DevsitePlugin",
-      "serializationFormat": "JSON",
-      "values": "{ \"projectPath\": \"androidx\", \"excludedPackages\": [ \".*excluded.*\" ], \"javaDocsPath\": \"\", \"kotlinDocsPath\": \"kotlin\", \"annotationsNotToDisplay\": [ \"java.lang.Override\", \"kotlin.ParameterName\" ] }"
-    }
-  ]
-}
-