mirror of
https://github.com/tadfisher/gradle2nix.git
synced 2026-01-12 07:50:53 -05:00
Rewrite based on code from the GitHub Dependency Graph Gradle Plugin
This commit is contained in:
9
app/src/main/kotlin/org/nixos/gradle2nix/Artifact.kt
Normal file
9
app/src/main/kotlin/org/nixos/gradle2nix/Artifact.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package org.nixos.gradle2nix
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Artifact(
|
||||
val urls: List<String>,
|
||||
val hash: String,
|
||||
)
|
||||
@@ -1,50 +0,0 @@
|
||||
package org.nixos.gradle2nix
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NixGradleEnv(
|
||||
val name: String,
|
||||
val version: String,
|
||||
val path: String,
|
||||
val gradle: DefaultGradle,
|
||||
val dependencies: Map<String, List<DefaultArtifact>>
|
||||
)
|
||||
|
||||
fun buildEnv(builds: Map<String, DefaultBuild>): Map<String, NixGradleEnv> =
|
||||
builds.mapValues { (path, build) ->
|
||||
NixGradleEnv(
|
||||
name = build.rootProject.name,
|
||||
version = build.rootProject.version,
|
||||
path = path,
|
||||
gradle = build.gradle,
|
||||
dependencies = mapOf(
|
||||
"settings" to build.settingsDependencies,
|
||||
"plugin" to build.pluginDependencies,
|
||||
"buildscript" to build.rootProject.collectDependencies(DefaultProject::buildscriptDependencies),
|
||||
"project" to build.rootProject.collectDependencies(DefaultProject::projectDependencies)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun DefaultProject.collectDependencies(
|
||||
chooser: DefaultProject.() -> List<DefaultArtifact>
|
||||
): List<DefaultArtifact> {
|
||||
val result = mutableMapOf<ArtifactIdentifier, DefaultArtifact>()
|
||||
mergeRepo(result, chooser())
|
||||
for (child in children) {
|
||||
mergeRepo(result, child.collectDependencies(chooser))
|
||||
}
|
||||
return result.values.toList()
|
||||
}
|
||||
|
||||
private fun mergeRepo(
|
||||
base: MutableMap<ArtifactIdentifier, DefaultArtifact>,
|
||||
extra: List<DefaultArtifact>
|
||||
) {
|
||||
extra.forEach { artifact ->
|
||||
base.merge(artifact.id, artifact) { old, new ->
|
||||
old.copy(urls = old.urls.union(new.urls).toList())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,19 +13,31 @@ fun connect(config: Config): ProjectConnection =
|
||||
.forProjectDirectory(config.projectDir)
|
||||
.connect()
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
fun ProjectConnection.getBuildModel(config: Config, path: String): DefaultBuild {
|
||||
return model(Build::class.java).apply {
|
||||
addArguments(
|
||||
"--init-script=$shareDir/init.gradle",
|
||||
"-Porg.nixos.gradle2nix.configurations=${config.configurations.joinToString(",")}",
|
||||
"-Porg.nixos.gradle2nix.subprojects=${config.subprojects.joinToString(",")}"
|
||||
)
|
||||
if (config.gradleArgs != null) addArguments(config.gradleArgs)
|
||||
if (path.isNotEmpty()) addArguments("--project-dir=$path")
|
||||
if (!config.quiet) {
|
||||
setStandardOutput(System.err)
|
||||
setStandardError(System.err)
|
||||
fun ProjectConnection.build(
|
||||
config: Config,
|
||||
) {
|
||||
newBuild()
|
||||
.apply {
|
||||
if (config.tasks.isNotEmpty()) {
|
||||
forTasks(*config.tasks.toTypedArray())
|
||||
} else {
|
||||
forTasks(RESOLVE_ALL_TASK)
|
||||
}
|
||||
addArguments(config.gradleArgs)
|
||||
addArguments(
|
||||
"--init-script=${config.appHome}/init.gradle",
|
||||
"--write-verification-metadata", "sha256"
|
||||
)
|
||||
if (config.projectFilter != null) {
|
||||
addArguments("-D${PARAM_INCLUDE_PROJECTS}")
|
||||
}
|
||||
if (config.configurationFilter != null) {
|
||||
addArguments("-D${PARAM_INCLUDE_CONFIGURATIONS}")
|
||||
}
|
||||
if (config.logger.verbose) {
|
||||
setStandardOutput(System.err)
|
||||
setStandardError(System.err)
|
||||
}
|
||||
}
|
||||
}.get().let { DefaultBuild(it) }
|
||||
.run()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.nixos.gradle2nix
|
||||
|
||||
import java.io.PrintStream
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class Logger(
|
||||
val out: PrintStream = System.err,
|
||||
@@ -9,9 +10,9 @@ class Logger(
|
||||
|
||||
val log: (String) -> Unit = { if (verbose) out.println(it) }
|
||||
val warn: (String) -> Unit = { out.println("Warning: $it")}
|
||||
val error: (String) -> Unit = {
|
||||
val error: (String) -> Nothing = {
|
||||
out.println("Error: $it")
|
||||
System.exit(1)
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
operator fun component1() = log
|
||||
|
||||
@@ -1,102 +1,97 @@
|
||||
package org.nixos.gradle2nix
|
||||
|
||||
import com.github.ajalt.clikt.completion.CompletionCandidates
|
||||
import com.github.ajalt.clikt.core.CliktCommand
|
||||
import com.github.ajalt.clikt.core.context
|
||||
import com.github.ajalt.clikt.output.CliktHelpFormatter
|
||||
import com.github.ajalt.clikt.parameters.arguments.ProcessedArgument
|
||||
import com.github.ajalt.clikt.parameters.arguments.argument
|
||||
import com.github.ajalt.clikt.parameters.arguments.convert
|
||||
import com.github.ajalt.clikt.parameters.arguments.default
|
||||
import com.github.ajalt.clikt.parameters.arguments.multiple
|
||||
import com.github.ajalt.clikt.parameters.options.default
|
||||
import com.github.ajalt.clikt.parameters.options.flag
|
||||
import com.github.ajalt.clikt.parameters.options.multiple
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
import com.github.ajalt.clikt.parameters.options.validate
|
||||
import com.github.ajalt.clikt.parameters.types.file
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import java.io.File
|
||||
|
||||
val shareDir: String = System.getProperty("org.nixos.gradle2nix.share")
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.encodeToStream
|
||||
|
||||
data class Config(
|
||||
val appHome: File,
|
||||
val gradleHome: File,
|
||||
val gradleVersion: String?,
|
||||
val gradleArgs: String?,
|
||||
val configurations: List<String>,
|
||||
val gradleArgs: List<String>,
|
||||
val projectFilter: String?,
|
||||
val configurationFilter: String?,
|
||||
val projectDir: File,
|
||||
val includes: List<File>,
|
||||
val subprojects: List<String>,
|
||||
val buildSrc: Boolean,
|
||||
val quiet: Boolean
|
||||
) {
|
||||
val allProjects = listOf(projectDir) + includes
|
||||
val tasks: List<String>,
|
||||
val logger: Logger,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
private val JsonFormat = Json {
|
||||
prettyPrint = true
|
||||
prettyPrintIndent = " "
|
||||
}
|
||||
|
||||
class Main : CliktCommand(
|
||||
class Gradle2Nix : CliktCommand(
|
||||
name = "gradle2nix"
|
||||
) {
|
||||
private val gradleVersion: String? by option("--gradle-version", "-g",
|
||||
private val gradleVersion: String? by option(
|
||||
"--gradle-version", "-g",
|
||||
metavar = "VERSION",
|
||||
help = "Use a specific Gradle version")
|
||||
help = "Use a specific Gradle version"
|
||||
)
|
||||
|
||||
private val gradleArgs: String? by option("--gradle-args", "-a",
|
||||
metavar = "ARGS",
|
||||
help = "Extra arguments to pass to Gradle")
|
||||
private val projectFilter: String? by option(
|
||||
"--projects", "-p",
|
||||
metavar = "REGEX",
|
||||
help = "Regex to filter Gradle projects (default: include all projects)"
|
||||
)
|
||||
|
||||
private val configurations: List<String> by option("--configuration", "-c",
|
||||
metavar = "NAME",
|
||||
help = "Add a configuration to resolve (default: all configurations)")
|
||||
.multiple()
|
||||
private val configurationFilter: String? by option(
|
||||
"--configurations", "-c",
|
||||
metavar = "REGEX",
|
||||
help = "Regex to filter Gradle configurations (default: include all configurations)")
|
||||
|
||||
private val includes: List<File> by option("--include", "-i",
|
||||
metavar = "DIR",
|
||||
help = "Add an additional project to include")
|
||||
.file(mustExist = true, canBeFile = false, canBeDir = true, mustBeReadable = true)
|
||||
.multiple()
|
||||
.validate { files ->
|
||||
val failures = files.filterNot { it.isProjectRoot() }
|
||||
if (failures.isNotEmpty()) {
|
||||
val message = failures.joinToString("\n ")
|
||||
fail("Included builds are not Gradle projects:\n$message\n" +
|
||||
"Gradle projects must contain a settings.gradle or settings.gradle.kts script.")
|
||||
}
|
||||
}
|
||||
|
||||
private val subprojects: List<String> by option("--project", "-p",
|
||||
metavar = "PATH",
|
||||
help = "Only resolve these subproject paths, e.g. ':', or ':sub:project' (default: all projects)")
|
||||
.multiple()
|
||||
.validate { paths ->
|
||||
val failures = paths.filterNot { it.startsWith(":") }
|
||||
if (failures.isNotEmpty()) {
|
||||
val message = failures.joinToString("\n ")
|
||||
fail("Subproject paths must be absolute:\n$message\n" +
|
||||
"Paths are in the form ':parent:child'.")
|
||||
}
|
||||
}
|
||||
|
||||
val outDir: File? by option("--out-dir", "-o",
|
||||
val outDir: File? by option(
|
||||
"--out-dir", "-o",
|
||||
metavar = "DIR",
|
||||
help = "Path to write generated files (default: PROJECT-DIR)")
|
||||
.file(canBeFile = false, canBeDir = true)
|
||||
|
||||
val envFile: String by option("--env", "-e",
|
||||
val envFile: String by option(
|
||||
"--env", "-e",
|
||||
metavar = "FILENAME",
|
||||
help = "Prefix for environment files (.json and .nix)")
|
||||
.default("gradle-env")
|
||||
|
||||
private val buildSrc: Boolean by option("--build-src", "-b", help = "Include buildSrc project (default: true)")
|
||||
.flag("--no-build-src", "-nb", default = true)
|
||||
|
||||
private val quiet: Boolean by option("--quiet", "-q", help = "Disable logging")
|
||||
.flag(default = false)
|
||||
|
||||
private val projectDir: File by argument("PROJECT-DIR", help = "Path to the project root (default: .)")
|
||||
.projectDir()
|
||||
.default(File("."))
|
||||
private val projectDir: File by option(
|
||||
"--projectDir", "-d",
|
||||
metavar = "PROJECT-DIR",
|
||||
help = "Path to the project root (default: .)")
|
||||
.file()
|
||||
.default(File("."), "Current directory")
|
||||
.validate { file ->
|
||||
if (!file.exists()) fail("Directory \"$file\" does not exist.")
|
||||
if (file.isFile) fail("Directory \"$file\" is a file.")
|
||||
if (!file.canRead()) fail("Directory \"$file\" is not readable.")
|
||||
if (!file.isProjectRoot()) fail("Directory \"$file\" is not a Gradle project.")
|
||||
}
|
||||
|
||||
private val tasks: List<String> by option(
|
||||
"--tasks", "-t",
|
||||
metavar = "TASKS",
|
||||
help = "Gradle tasks to run"
|
||||
).multiple()
|
||||
|
||||
private val gradleArgs: List<String> by argument(
|
||||
name = "ARGS",
|
||||
help = "Extra arguments to pass to Gradle"
|
||||
).multiple()
|
||||
|
||||
init {
|
||||
context {
|
||||
@@ -107,65 +102,59 @@ class Main : CliktCommand(
|
||||
// Visible for testing
|
||||
lateinit var config: Config
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
override fun run() {
|
||||
val appHome = System.getProperty("org.nixos.gradle2nix.share")
|
||||
if (appHome == null) {
|
||||
System.err.println("Error: could not locate the /share directory in the gradle2nix installation")
|
||||
}
|
||||
val gradleHome = System.getenv("GRADLE_USER_HOME")?.let(::File) ?: File("${System.getProperty("user.home")}/.gradle")
|
||||
val logger = Logger(verbose = !quiet)
|
||||
|
||||
config = Config(
|
||||
File(appHome),
|
||||
gradleHome,
|
||||
gradleVersion,
|
||||
gradleArgs,
|
||||
configurations,
|
||||
projectFilter,
|
||||
configurationFilter,
|
||||
projectDir,
|
||||
includes,
|
||||
subprojects,
|
||||
buildSrc,
|
||||
quiet
|
||||
tasks,
|
||||
logger
|
||||
)
|
||||
val (log, _, _) = Logger(verbose = !config.quiet)
|
||||
|
||||
val paths = resolveProjects(config).map { p ->
|
||||
p.toRelativeString(config.projectDir)
|
||||
}
|
||||
val (log, _, error) = logger
|
||||
|
||||
val models = connect(config).use { connection ->
|
||||
paths.associateWith { project ->
|
||||
log("Resolving project model: ${project.takeIf { it.isNotEmpty() } ?: "root project"}...")
|
||||
connection.getBuildModel(config, project)
|
||||
val metadata = File("$projectDir/gradle/verification-metadata.xml")
|
||||
if (metadata.exists()) {
|
||||
val backup = metadata.resolveSibling("verification-metadata.xml.bak")
|
||||
if (metadata.renameTo(backup)) {
|
||||
Runtime.getRuntime().addShutdownHook(Thread {
|
||||
metadata.delete()
|
||||
backup.renameTo(metadata)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
log("Building environment...")
|
||||
val nixGradleEnv = buildEnv(models)
|
||||
connect(config).use { connection ->
|
||||
connection.build(config)
|
||||
}
|
||||
|
||||
val dependencies = try {
|
||||
processDependencies(config)
|
||||
} catch (e: Throwable) {
|
||||
error("Dependency parsing failed: ${e.message}")
|
||||
}
|
||||
|
||||
val outDir = outDir ?: projectDir
|
||||
|
||||
val json = outDir.resolve("$envFile.json")
|
||||
log("Writing environment to $json")
|
||||
|
||||
json.sink().buffer().use { out ->
|
||||
Moshi.Builder().build()
|
||||
.adapter<Map<String, NixGradleEnv>>(
|
||||
Types.newParameterizedType(Map::class.java, String::class.java, NixGradleEnv::class.java)
|
||||
)
|
||||
.indent(" ")
|
||||
.toJson(out, nixGradleEnv)
|
||||
out.flush()
|
||||
}
|
||||
|
||||
val nix = outDir.resolve("$envFile.nix")
|
||||
log("Writing Nix script to $nix")
|
||||
|
||||
File(shareDir).resolve("gradle-env.nix").copyTo(nix, overwrite = true)
|
||||
}
|
||||
}
|
||||
|
||||
fun ProcessedArgument<String, String>.projectDir(): ProcessedArgument<File, File> {
|
||||
return convert(completionCandidates = CompletionCandidates.Path) {
|
||||
File(it).also { file ->
|
||||
if (!file.exists()) fail("Directory \"$file\" does not exist.")
|
||||
if (file.isFile) fail("Directory \"$file\" is a file.")
|
||||
if (!file.canRead()) fail("Directory \"$file\" is not readable.")
|
||||
if (!file.isProjectRoot()) fail("Directory \"$file\" is not a Gradle project.")
|
||||
json.outputStream().buffered().use { output ->
|
||||
JsonFormat.encodeToStream(dependencies, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) = Main().main(args)
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
Gradle2Nix().main(args)
|
||||
}
|
||||
|
||||
207
app/src/main/kotlin/org/nixos/gradle2nix/Process.kt
Normal file
207
app/src/main/kotlin/org/nixos/gradle2nix/Process.kt
Normal file
@@ -0,0 +1,207 @@
|
||||
package org.nixos.gradle2nix
|
||||
|
||||
import java.io.File
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import okio.ByteString.Companion.decodeHex
|
||||
import okio.HashingSource
|
||||
import okio.blackholeSink
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import org.nixos.gradle2nix.dependency.ModuleComponentIdentifier
|
||||
import org.nixos.gradle2nix.dependencygraph.model.DependencyCoordinates
|
||||
import org.nixos.gradle2nix.dependencygraph.model.Repository
|
||||
import org.nixos.gradle2nix.dependencygraph.model.ResolvedConfiguration
|
||||
import org.nixos.gradle2nix.metadata.ArtifactVerificationMetadata
|
||||
import org.nixos.gradle2nix.metadata.Checksum
|
||||
import org.nixos.gradle2nix.metadata.ChecksumKind
|
||||
import org.nixos.gradle2nix.metadata.ComponentVerificationMetadata
|
||||
import org.nixos.gradle2nix.metadata.DependencyVerificationsXmlReader
|
||||
import org.nixos.gradle2nix.metadata.DependencyVerifier
|
||||
|
||||
// Local Maven repository for testing
|
||||
private val m2 = System.getProperty("org.nixos.gradle2nix.m2")
|
||||
|
||||
private fun shouldSkipRepository(repository: Repository): Boolean {
|
||||
return repository.artifactResources.all { it.startsWith("file:") && (m2 == null || !it.startsWith(m2)) } ||
|
||||
repository.metadataResources.all { it.startsWith("file:") && (m2 == null || !it.startsWith(m2)) }
|
||||
}
|
||||
|
||||
fun processDependencies(config: Config): Map<String, Map<String, Artifact>> {
|
||||
val verifier = readVerificationMetadata(config)
|
||||
val configurations = readDependencyGraph(config)
|
||||
|
||||
val repositories = configurations
|
||||
.flatMap { it.repositories }
|
||||
.associateBy { it.id }
|
||||
.filterNot { (id, repo) ->
|
||||
if (shouldSkipRepository(repo)) {
|
||||
config.logger.warn("$id: all URLs are files; skipping")
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
if (repositories.isEmpty()) {
|
||||
config.logger.warn("no repositories found in any configuration")
|
||||
return emptyMap()
|
||||
}
|
||||
|
||||
return configurations.asSequence()
|
||||
.flatMap { it.allDependencies.asSequence() }
|
||||
.groupBy { it.id }
|
||||
.mapNotNull { (id, dependencies) ->
|
||||
val deps = dependencies.toSet()
|
||||
if (deps.isEmpty()) {
|
||||
config.logger.warn("$id: no resolved dependencies in dependency graph")
|
||||
return@mapNotNull null
|
||||
}
|
||||
val coordinates = deps.first().coordinates
|
||||
val componentId = ModuleComponentIdentifier(
|
||||
coordinates.group,
|
||||
coordinates.module,
|
||||
coordinates.version
|
||||
)
|
||||
val metadata = verifier.verificationMetadata[componentId]
|
||||
?: verifyComponentFilesInCache(config, componentId)
|
||||
if (metadata == null) {
|
||||
config.logger.warn("$id: not present in metadata or cache; skipping")
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
val repoIds = dependencies.mapNotNull { it.repository }
|
||||
if (repoIds.isEmpty()) {
|
||||
config.logger.warn("$id: no repository ids in dependency graph; skipping")
|
||||
return@mapNotNull null
|
||||
}
|
||||
val repos = repoIds.mapNotNull(repositories::get)
|
||||
if (repos.isEmpty()) {
|
||||
config.logger.warn("$id: no repositories found for repository ids $repoIds; skipping")
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
id to metadata.artifactVerifications.associate { meta ->
|
||||
meta.artifactName to Artifact(
|
||||
urls = repos
|
||||
.flatMap { repository -> artifactUrls(coordinates, meta, repository) }
|
||||
.distinct(),
|
||||
hash = meta.checksums.maxBy { c -> c.kind.ordinal }.toSri()
|
||||
)
|
||||
}
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
|
||||
private fun readVerificationMetadata(config: Config): DependencyVerifier {
|
||||
return config.projectDir.resolve("gradle/verification-metadata.xml")
|
||||
.inputStream()
|
||||
.buffered()
|
||||
.use { input -> DependencyVerificationsXmlReader.readFromXml(input) }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
private fun readDependencyGraph(config: Config): List<ResolvedConfiguration> {
|
||||
return config.projectDir.resolve("build/reports/nix-dependency-graph/dependency-graph.json")
|
||||
.inputStream()
|
||||
.buffered()
|
||||
.use { input -> Json.decodeFromStream(input) }
|
||||
}
|
||||
|
||||
private fun verifyComponentFilesInCache(
|
||||
config: Config,
|
||||
component: ModuleComponentIdentifier
|
||||
): ComponentVerificationMetadata? {
|
||||
val cacheDir = config.gradleHome.resolve("caches/modules-2/files-2.1/${component.group}/${component.module}/${component.version}")
|
||||
if (!cacheDir.exists()) {
|
||||
return null
|
||||
}
|
||||
val verifications = cacheDir.walkBottomUp().filter { it.isFile }.map { f ->
|
||||
ArtifactVerificationMetadata(
|
||||
f.name,
|
||||
listOf(Checksum(ChecksumKind.sha256, f.sha256()))
|
||||
)
|
||||
}
|
||||
config.logger.log("$component: obtained artifact hashes from Gradle cache.")
|
||||
return ComponentVerificationMetadata(component, verifications.toList())
|
||||
}
|
||||
|
||||
private fun File.sha256(): String {
|
||||
val source = HashingSource.sha256(source())
|
||||
source.buffer().readAll(blackholeSink())
|
||||
return source.hash.hex()
|
||||
}
|
||||
|
||||
private fun Checksum.toSri(): String {
|
||||
val hash = value.decodeHex().base64()
|
||||
return when (kind) {
|
||||
ChecksumKind.md5 -> "md5-$hash"
|
||||
ChecksumKind.sha1 -> "sha1-$hash"
|
||||
ChecksumKind.sha256 -> "sha256-$hash"
|
||||
ChecksumKind.sha512 -> "sha512-$hash"
|
||||
}
|
||||
}
|
||||
|
||||
private fun artifactUrls(
|
||||
coordinates: DependencyCoordinates,
|
||||
metadata: ArtifactVerificationMetadata,
|
||||
repository: Repository
|
||||
): List<String> {
|
||||
val groupAsPath = coordinates.group.replace(".", "/")
|
||||
|
||||
val attributes = mutableMapOf(
|
||||
"organisation" to if (repository.m2Compatible) groupAsPath else coordinates.group,
|
||||
"module" to coordinates.module,
|
||||
"revision" to coordinates.version,
|
||||
) + fileAttributes(metadata.artifactName, coordinates.version)
|
||||
|
||||
val resources = when (attributes["ext"]) {
|
||||
"pom" -> if ("mavenPom" in repository.metadataSources) repository.metadataResources else repository.artifactResources
|
||||
"xml" -> if ("ivyDescriptor" in repository.metadataSources) repository.metadataResources else repository.artifactResources
|
||||
"module" -> if ("gradleMetadata" in repository.metadataSources || "ignoreGradleMetadataRedirection" !in repository.metadataSources) {
|
||||
repository.metadataResources
|
||||
} else {
|
||||
repository.artifactResources
|
||||
}
|
||||
else -> repository.artifactResources
|
||||
}
|
||||
|
||||
val urls = mutableListOf<String>()
|
||||
|
||||
for (resource in resources) {
|
||||
val location = attributes.entries.fold(fill(resource, attributes)) { acc, (key, value) ->
|
||||
acc.replace("[$key]", value)
|
||||
}
|
||||
if (location.none { it == '[' || it == ']' }) {
|
||||
urls.add(location)
|
||||
}
|
||||
}
|
||||
|
||||
return urls
|
||||
}
|
||||
|
||||
private val optionalRegex = Regex("\\(([^)]+)\\)")
|
||||
private val attrRegex = Regex("\\[([^]]+)]")
|
||||
|
||||
private fun fill(template: String, attributes: Map<String, String>): String {
|
||||
return optionalRegex.replace(template) { match ->
|
||||
attrRegex.find(match.value)?.groupValues?.get(1)?.let { attr ->
|
||||
attributes[attr]?.takeIf { it.isNotBlank() }?.let { value ->
|
||||
match.groupValues[1].replace("[$attr]", value)
|
||||
}
|
||||
} ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
// Gradle persists artifacts with the Maven artifact pattern, which may not match the repository's pattern.
|
||||
private fun fileAttributes(file: String, version: String): Map<String, String> {
|
||||
val parts = Regex("(.+)-$version(-([^.]+))?(\\.(.+))?").matchEntire(file) ?: return emptyMap()
|
||||
|
||||
val (artifact, _, classifier, _, ext) = parts.destructured
|
||||
|
||||
return buildMap {
|
||||
put("artifact", artifact)
|
||||
put("classifier", classifier)
|
||||
put("ext", ext)
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,5 @@ package org.nixos.gradle2nix
|
||||
|
||||
import java.io.File
|
||||
|
||||
fun resolveProjects(config: Config) = config.allProjects.run {
|
||||
if (config.buildSrc) {
|
||||
flatMap { listOfNotNull(it, it.findBuildSrc()) }
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
fun File.findBuildSrc(): File? =
|
||||
resolve("buildSrc").takeIf { it.isDirectory }
|
||||
|
||||
fun File.isProjectRoot(): Boolean =
|
||||
isDirectory && (resolve("settings.gradle").isFile || resolve("settings.gradle.kts").isFile)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.nixos.gradle2nix.dependency
|
||||
|
||||
/**
|
||||
* An opaque immutable identifier for an artifact that belongs to some component instance.
|
||||
*/
|
||||
interface ComponentArtifactIdentifier {
|
||||
/**
|
||||
* Returns the id of the component that this artifact belongs to.
|
||||
*/
|
||||
val componentIdentifier: ComponentIdentifier
|
||||
|
||||
/**
|
||||
* Returns some human-consumable display name for this artifact.
|
||||
*/
|
||||
val displayName: String
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.nixos.gradle2nix.dependency
|
||||
|
||||
/**
|
||||
* An opaque immutable identifier for a component instance. There are various sub-interfaces that expose specific details about the identifier.
|
||||
*/
|
||||
interface ComponentIdentifier {
|
||||
/**
|
||||
* Returns a human-consumable display name for this identifier.
|
||||
*
|
||||
* @return Component identifier display name
|
||||
*/
|
||||
val displayName: String
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.nixos.gradle2nix.dependency
|
||||
|
||||
/**
|
||||
* An immutable identifier for an artifact that belongs to some module version.
|
||||
*/
|
||||
interface ModuleComponentArtifactIdentifier : ComponentArtifactIdentifier {
|
||||
/**
|
||||
* Returns the id of the component that this artifact belongs to.
|
||||
*/
|
||||
override val componentIdentifier: ModuleComponentIdentifier
|
||||
|
||||
/**
|
||||
* Returns a file base name that can be used for this artifact.
|
||||
*/
|
||||
val fileName: String
|
||||
}
|
||||
|
||||
data class DefaultModuleComponentArtifactIdentifier(
|
||||
override val componentIdentifier: ModuleComponentIdentifier,
|
||||
override val fileName: String,
|
||||
) : ModuleComponentArtifactIdentifier {
|
||||
override val displayName: String get() = "$fileName ($componentIdentifier)"
|
||||
|
||||
override fun toString(): String = displayName
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.nixos.gradle2nix.dependency
|
||||
|
||||
/**
|
||||
* An identifier for a component instance which is available as a module version.
|
||||
*/
|
||||
interface ModuleComponentIdentifier : ComponentIdentifier {
|
||||
/**
|
||||
* The module group of the component.
|
||||
*
|
||||
* @return Component group
|
||||
*/
|
||||
val group: String
|
||||
|
||||
/**
|
||||
* The module name of the component.
|
||||
*
|
||||
* @return Component module
|
||||
*/
|
||||
val module: String
|
||||
|
||||
/**
|
||||
* The module version of the component.
|
||||
*
|
||||
* @return Component version
|
||||
*/
|
||||
val version: String
|
||||
|
||||
/**
|
||||
* The module identifier of the component. Returns the same information
|
||||
* as [group] and [module].
|
||||
*
|
||||
* @return the module identifier
|
||||
*/
|
||||
val moduleIdentifier: ModuleIdentifier
|
||||
}
|
||||
|
||||
data class DefaultModuleComponentIdentifier(
|
||||
override val moduleIdentifier: ModuleIdentifier,
|
||||
override val version: String,
|
||||
) : ModuleComponentIdentifier {
|
||||
override val group: String
|
||||
get() = moduleIdentifier.group
|
||||
|
||||
override val module: String
|
||||
get() = moduleIdentifier.name
|
||||
|
||||
override val displayName: String
|
||||
get() = "$group:$module:$version"
|
||||
|
||||
override fun toString(): String = displayName
|
||||
}
|
||||
|
||||
|
||||
fun ModuleComponentIdentifier(
|
||||
group: String,
|
||||
module: String,
|
||||
version: String
|
||||
): ModuleComponentIdentifier = DefaultModuleComponentIdentifier(
|
||||
DefaultModuleIdentifier(group, module),
|
||||
version
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2012 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.nixos.gradle2nix.dependency
|
||||
|
||||
/**
|
||||
* The identifier of a module.
|
||||
*/
|
||||
interface ModuleIdentifier {
|
||||
/**
|
||||
* The group of the module.
|
||||
*
|
||||
* @return module group
|
||||
*/
|
||||
val group: String
|
||||
|
||||
/**
|
||||
* The name of the module.
|
||||
*
|
||||
* @return module name
|
||||
*/
|
||||
val name: String
|
||||
}
|
||||
|
||||
data class DefaultModuleIdentifier(
|
||||
override val group: String,
|
||||
override val name: String,
|
||||
) : ModuleIdentifier
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.nixos.gradle2nix.metadata
|
||||
|
||||
data class ArtifactVerificationMetadata(
|
||||
val artifactName: String,
|
||||
val checksums: List<Checksum>
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.nixos.gradle2nix.metadata
|
||||
|
||||
/**
|
||||
* Internal representation of a checksum, aimed at *verification*.
|
||||
* A checksum consists of a kind (md5, sha1, ...), a value, but also
|
||||
* provides *alternatives*. Alternatives are checksums which are
|
||||
* deemed trusted, because sometimes in a single build we can see different
|
||||
* checksums for the same module, because they are sourced from different
|
||||
* repositories.
|
||||
*
|
||||
* In theory, this shouldn't be allowed. However, it's often the case that
|
||||
* an artifact, in particular _metadata artifacts_ (POM files, ...) differ
|
||||
* from one repository to the other (either by end of lines, additional line
|
||||
* at the end of the file, ...). Because they are different doesn't mean that
|
||||
* they are compromised, so this is a facility for the user to declare "I know
|
||||
* I should use a single source of truth but the infrastructure is hard or
|
||||
* impossible to fix so let's trust this source".
|
||||
*
|
||||
* In addition to the list of alternatives, a checksum also provides a source,
|
||||
* which is documentation to explain where a checksum was found.
|
||||
*/
|
||||
data class Checksum(
|
||||
val kind: ChecksumKind,
|
||||
val value: String,
|
||||
val alternatives: Set<String> = emptySet(),
|
||||
val origin: String? = null,
|
||||
val reason: String? = null
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.nixos.gradle2nix.metadata
|
||||
|
||||
enum class ChecksumKind(val algorithm: String) {
|
||||
md5("MD5"),
|
||||
sha1("SHA1"),
|
||||
sha256("SHA-256"),
|
||||
sha512("SHA-512");
|
||||
|
||||
companion object {
|
||||
private val SORTED_BY_SECURITY: List<ChecksumKind> = listOf(sha512, sha256, sha1, md5)
|
||||
fun mostSecureFirst(): List<ChecksumKind> {
|
||||
return SORTED_BY_SECURITY
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.nixos.gradle2nix.metadata
|
||||
|
||||
import org.nixos.gradle2nix.dependency.ModuleComponentIdentifier
|
||||
|
||||
data class ComponentVerificationMetadata(
|
||||
val componentId: ModuleComponentIdentifier,
|
||||
val artifactVerifications: List<ArtifactVerificationMetadata>,
|
||||
)
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.nixos.gradle2nix.metadata
|
||||
|
||||
import org.nixos.gradle2nix.dependency.ModuleComponentArtifactIdentifier
|
||||
import org.nixos.gradle2nix.dependency.ModuleComponentIdentifier
|
||||
|
||||
class DependencyVerificationConfiguration(
|
||||
val trustedArtifacts: List<TrustCoordinates> = emptyList(),
|
||||
) {
|
||||
data class TrustCoordinates internal constructor(
|
||||
val group: String?,
|
||||
val name: String?,
|
||||
val version: String?,
|
||||
val fileName: String?,
|
||||
val isRegex: Boolean,
|
||||
val reason: String?
|
||||
) : Comparable<TrustCoordinates> {
|
||||
|
||||
fun matches(id: ModuleComponentArtifactIdentifier): Boolean {
|
||||
val moduleComponentIdentifier: ModuleComponentIdentifier = id.componentIdentifier
|
||||
return (matches(group, moduleComponentIdentifier.group)
|
||||
&& matches(name, moduleComponentIdentifier.module)
|
||||
&& matches(version, moduleComponentIdentifier.version)
|
||||
&& matches(fileName, id.fileName))
|
||||
}
|
||||
|
||||
private fun matches(value: String?, expr: String): Boolean {
|
||||
if (value == null) {
|
||||
return true
|
||||
}
|
||||
return if (!isRegex) {
|
||||
expr == value
|
||||
} else expr.matches(value.toRegex())
|
||||
}
|
||||
|
||||
override fun compareTo(other: TrustCoordinates): Int {
|
||||
val regexComparison = isRegex.compareTo(other.isRegex)
|
||||
if (regexComparison != 0) {
|
||||
return regexComparison
|
||||
}
|
||||
val groupComparison = compareNullableStrings(
|
||||
group, other.group
|
||||
)
|
||||
if (groupComparison != 0) {
|
||||
return groupComparison
|
||||
}
|
||||
val nameComparison = compareNullableStrings(
|
||||
name, other.name
|
||||
)
|
||||
if (nameComparison != 0) {
|
||||
return nameComparison
|
||||
}
|
||||
val versionComparison = compareNullableStrings(
|
||||
version, other.version
|
||||
)
|
||||
if (versionComparison != 0) {
|
||||
return versionComparison
|
||||
}
|
||||
val fileNameComparison = compareNullableStrings(
|
||||
fileName, other.fileName
|
||||
)
|
||||
return if (fileNameComparison != 0) {
|
||||
fileNameComparison
|
||||
} else compareNullableStrings(
|
||||
reason, other.reason
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun compareNullableStrings(first: String?, second: String?): Int {
|
||||
if (first == null) {
|
||||
return if (second == null) {
|
||||
0
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
} else if (second == null) {
|
||||
return 1
|
||||
}
|
||||
return first.compareTo(second)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.nixos.gradle2nix.metadata
|
||||
|
||||
internal object DependencyVerificationXmlTags {
|
||||
const val ALSO_TRUST = "also-trust"
|
||||
const val ARTIFACT = "artifact"
|
||||
const val COMPONENT = "component"
|
||||
const val COMPONENTS = "components"
|
||||
const val CONFIG = "configuration"
|
||||
const val ENABLED = "enabled"
|
||||
const val FILE = "file"
|
||||
const val GROUP = "group"
|
||||
const val ID = "id"
|
||||
const val IGNORED_KEY = "ignored-key"
|
||||
const val IGNORED_KEYS = "ignored-keys"
|
||||
const val KEY_SERVER = "key-server"
|
||||
const val KEY_SERVERS = "key-servers"
|
||||
const val NAME = "name"
|
||||
const val ORIGIN = "origin"
|
||||
const val PGP = "pgp"
|
||||
const val REASON = "reason"
|
||||
const val REGEX = "regex"
|
||||
const val TRUST = "trust"
|
||||
const val TRUSTED_ARTIFACTS = "trusted-artifacts"
|
||||
const val TRUSTED_KEY = "trusted-key"
|
||||
const val TRUSTED_KEYS = "trusted-keys"
|
||||
const val TRUSTING = "trusting"
|
||||
const val URI = "uri"
|
||||
const val VALUE = "value"
|
||||
const val VERIFICATION_METADATA = "verification-metadata"
|
||||
const val VERIFY_METADATA = "verify-metadata"
|
||||
const val VERIFY_SIGNATURES = "verify-signatures"
|
||||
const val VERSION = "version"
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
/*
|
||||
* Copyright 2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.nixos.gradle2nix.metadata
|
||||
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import javax.xml.parsers.ParserConfigurationException
|
||||
import javax.xml.parsers.SAXParser
|
||||
import javax.xml.parsers.SAXParserFactory
|
||||
import org.gradle.internal.UncheckedException
|
||||
import org.nixos.gradle2nix.dependency.DefaultModuleComponentArtifactIdentifier
|
||||
import org.nixos.gradle2nix.dependency.DefaultModuleComponentIdentifier
|
||||
import org.nixos.gradle2nix.dependency.DefaultModuleIdentifier
|
||||
import org.nixos.gradle2nix.dependency.ModuleComponentArtifactIdentifier
|
||||
import org.nixos.gradle2nix.dependency.ModuleComponentIdentifier
|
||||
import org.nixos.gradle2nix.dependency.ModuleIdentifier
|
||||
import org.nixos.gradle2nix.metadata.DependencyVerificationXmlTags.ALSO_TRUST
|
||||
import org.nixos.gradle2nix.metadata.DependencyVerificationXmlTags.ARTIFACT
|
||||
import org.nixos.gradle2nix.metadata.DependencyVerificationXmlTags.COMPONENT
|
||||
import org.nixos.gradle2nix.metadata.DependencyVerificationXmlTags.COMPONENTS
|
||||
import org.nixos.gradle2nix.metadata.DependencyVerificationXmlTags.CONFIG
|
||||
import org.nixos.gradle2nix.metadata.DependencyVerificationXmlTags.IGNORED_KEY
|
||||
import org.nixos.gradle2nix.metadata.DependencyVerificationXmlTags.IGNORED_KEYS
|
||||
import org.nixos.gradle2nix.metadata.DependencyVerificationXmlTags.KEY_SERVER
|
||||
import org.nixos.gradle2nix.metadata.DependencyVerificationXmlTags.KEY_SERVERS
|
||||
import org.nixos.gradle2nix.metadata.DependencyVerificationXmlTags.PGP
|
||||
import org.nixos.gradle2nix.metadata.DependencyVerificationXmlTags.TRUST
|
||||
import org.nixos.gradle2nix.metadata.DependencyVerificationXmlTags.TRUSTED_ARTIFACTS
|
||||
import org.nixos.gradle2nix.metadata.DependencyVerificationXmlTags.TRUSTED_KEY
|
||||
import org.nixos.gradle2nix.metadata.DependencyVerificationXmlTags.TRUSTED_KEYS
|
||||
import org.nixos.gradle2nix.metadata.DependencyVerificationXmlTags.TRUSTING
|
||||
import org.nixos.gradle2nix.metadata.DependencyVerificationXmlTags.VALUE
|
||||
import org.nixos.gradle2nix.metadata.DependencyVerificationXmlTags.VERIFICATION_METADATA
|
||||
import org.nixos.gradle2nix.metadata.DependencyVerificationXmlTags.VERIFY_METADATA
|
||||
import org.nixos.gradle2nix.metadata.DependencyVerificationXmlTags.VERIFY_SIGNATURES
|
||||
import org.xml.sax.Attributes
|
||||
import org.xml.sax.InputSource
|
||||
import org.xml.sax.SAXException
|
||||
import org.xml.sax.ext.DefaultHandler2
|
||||
|
||||
object DependencyVerificationsXmlReader {
|
||||
fun readFromXml(
|
||||
input: InputStream,
|
||||
builder: DependencyVerifierBuilder
|
||||
) {
|
||||
try {
|
||||
val saxParser = createSecureParser()
|
||||
val xmlReader = saxParser.xmlReader
|
||||
val handler = VerifiersHandler(builder)
|
||||
xmlReader.setProperty("http://xml.org/sax/properties/lexical-handler", handler)
|
||||
xmlReader.contentHandler = handler
|
||||
xmlReader.parse(InputSource(input))
|
||||
} catch (e: Exception) {
|
||||
throw IllegalStateException(
|
||||
"Unable to read dependency verification metadata",
|
||||
e
|
||||
)
|
||||
} finally {
|
||||
try {
|
||||
input.close()
|
||||
} catch (e: IOException) {
|
||||
throw UncheckedException.throwAsUncheckedException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun readFromXml(input: InputStream): DependencyVerifier {
|
||||
val builder = DependencyVerifierBuilder()
|
||||
readFromXml(input, builder)
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
@Throws(ParserConfigurationException::class, SAXException::class)
|
||||
private fun createSecureParser(): SAXParser {
|
||||
val spf = SAXParserFactory.newInstance()
|
||||
spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
|
||||
spf.setFeature("http://xml.org/sax/features/namespaces", false)
|
||||
spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
|
||||
return spf.newSAXParser()
|
||||
}
|
||||
|
||||
private class VerifiersHandler(private val builder: DependencyVerifierBuilder) : DefaultHandler2() {
|
||||
private var inMetadata = false
|
||||
private var inComponents = false
|
||||
private var inConfiguration = false
|
||||
private var inVerifyMetadata = false
|
||||
private var inVerifySignatures = false
|
||||
private var inTrustedArtifacts = false
|
||||
private var inKeyServers = false
|
||||
private var inIgnoredKeys = false
|
||||
private var inTrustedKeys = false
|
||||
private var inTrustedKey = false
|
||||
private var currentTrustedKey: String? = null
|
||||
private var currentComponent: ModuleComponentIdentifier? = null
|
||||
private var currentArtifact: ModuleComponentArtifactIdentifier? =
|
||||
null
|
||||
private var currentChecksum: ChecksumKind? = null
|
||||
|
||||
override fun startElement(uri: String, localName: String, qName: String, attributes: Attributes) {
|
||||
when (qName) {
|
||||
CONFIG -> inConfiguration =
|
||||
true
|
||||
|
||||
VERIFICATION_METADATA -> inMetadata =
|
||||
true
|
||||
|
||||
COMPONENTS -> {
|
||||
assertInMetadata()
|
||||
inComponents = true
|
||||
}
|
||||
|
||||
COMPONENT -> {
|
||||
assertInComponents()
|
||||
currentComponent = createComponentId(attributes)
|
||||
}
|
||||
|
||||
ARTIFACT -> {
|
||||
assertValidComponent()
|
||||
currentArtifact = createArtifactId(attributes)
|
||||
}
|
||||
|
||||
VERIFY_METADATA -> {
|
||||
assertInConfiguration(VERIFY_METADATA)
|
||||
inVerifyMetadata = true
|
||||
}
|
||||
|
||||
VERIFY_SIGNATURES -> {
|
||||
assertInConfiguration(VERIFY_SIGNATURES)
|
||||
inVerifySignatures = true
|
||||
}
|
||||
|
||||
TRUSTED_ARTIFACTS -> {
|
||||
assertInConfiguration(TRUSTED_ARTIFACTS)
|
||||
inTrustedArtifacts = true
|
||||
}
|
||||
|
||||
TRUSTED_KEY -> {
|
||||
assertContext(
|
||||
inTrustedKeys,
|
||||
TRUSTED_KEY,
|
||||
TRUSTED_KEYS
|
||||
)
|
||||
inTrustedKey = true
|
||||
}
|
||||
|
||||
TRUSTED_KEYS -> {
|
||||
assertInConfiguration(TRUSTED_KEYS)
|
||||
inTrustedKeys = true
|
||||
}
|
||||
|
||||
TRUST -> {
|
||||
assertInTrustedArtifacts()
|
||||
addTrustedArtifact(attributes)
|
||||
}
|
||||
|
||||
TRUSTING -> {
|
||||
assertContext(
|
||||
inTrustedKey,
|
||||
TRUSTING,
|
||||
TRUSTED_KEY
|
||||
)
|
||||
}
|
||||
|
||||
KEY_SERVERS -> {
|
||||
assertInConfiguration(KEY_SERVERS)
|
||||
inKeyServers = true
|
||||
}
|
||||
|
||||
KEY_SERVER -> {
|
||||
assertContext(
|
||||
inKeyServers,
|
||||
KEY_SERVER,
|
||||
KEY_SERVERS
|
||||
)
|
||||
}
|
||||
|
||||
IGNORED_KEYS -> {
|
||||
if (currentArtifact == null) {
|
||||
assertInConfiguration(IGNORED_KEYS)
|
||||
}
|
||||
inIgnoredKeys = true
|
||||
}
|
||||
|
||||
IGNORED_KEY -> {
|
||||
assertContext(
|
||||
inIgnoredKeys,
|
||||
IGNORED_KEY,
|
||||
IGNORED_KEYS
|
||||
)
|
||||
}
|
||||
|
||||
else -> if (currentChecksum != null && ALSO_TRUST == qName) {
|
||||
builder.addChecksum(
|
||||
currentArtifact!!,
|
||||
currentChecksum!!,
|
||||
getAttribute(attributes, VALUE),
|
||||
null,
|
||||
null
|
||||
)
|
||||
} else if (currentArtifact != null) {
|
||||
if (PGP != qName) {
|
||||
currentChecksum = enumValueOf<ChecksumKind>(qName)
|
||||
builder.addChecksum(
|
||||
currentArtifact!!,
|
||||
currentChecksum!!,
|
||||
getAttribute(
|
||||
attributes,
|
||||
VALUE
|
||||
),
|
||||
getNullableAttribute(
|
||||
attributes,
|
||||
DependencyVerificationXmlTags.ORIGIN
|
||||
),
|
||||
getNullableAttribute(
|
||||
attributes,
|
||||
DependencyVerificationXmlTags.REASON
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertInTrustedArtifacts() {
|
||||
assertContext(
|
||||
inTrustedArtifacts,
|
||||
TRUST,
|
||||
TRUSTED_ARTIFACTS
|
||||
)
|
||||
}
|
||||
|
||||
private fun addTrustedArtifact(attributes: Attributes) {
|
||||
var regex = false
|
||||
val regexAttr = getNullableAttribute(
|
||||
attributes,
|
||||
DependencyVerificationXmlTags.REGEX
|
||||
)
|
||||
if (regexAttr != null) {
|
||||
regex = regexAttr.toBoolean()
|
||||
}
|
||||
builder.addTrustedArtifact(
|
||||
getNullableAttribute(
|
||||
attributes,
|
||||
DependencyVerificationXmlTags.GROUP
|
||||
),
|
||||
getNullableAttribute(
|
||||
attributes,
|
||||
DependencyVerificationXmlTags.NAME
|
||||
),
|
||||
getNullableAttribute(
|
||||
attributes,
|
||||
DependencyVerificationXmlTags.VERSION
|
||||
),
|
||||
getNullableAttribute(
|
||||
attributes,
|
||||
DependencyVerificationXmlTags.FILE
|
||||
),
|
||||
regex,
|
||||
getNullableAttribute(
|
||||
attributes,
|
||||
DependencyVerificationXmlTags.REASON
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun readBoolean(ch: CharArray, start: Int, length: Int): Boolean {
|
||||
return String(ch, start, length).toBoolean()
|
||||
}
|
||||
|
||||
private fun assertInConfiguration(tag: String) {
|
||||
assertContext(
|
||||
inConfiguration,
|
||||
tag,
|
||||
DependencyVerificationXmlTags.CONFIG
|
||||
)
|
||||
}
|
||||
|
||||
private fun assertInComponents() {
|
||||
assertContext(
|
||||
inComponents,
|
||||
DependencyVerificationXmlTags.COMPONENT,
|
||||
DependencyVerificationXmlTags.COMPONENTS
|
||||
)
|
||||
}
|
||||
|
||||
private fun assertInMetadata() {
|
||||
assertContext(
|
||||
inMetadata,
|
||||
DependencyVerificationXmlTags.COMPONENTS,
|
||||
DependencyVerificationXmlTags.VERIFICATION_METADATA
|
||||
)
|
||||
}
|
||||
|
||||
private fun assertValidComponent() {
|
||||
assertContext(
|
||||
currentComponent != null,
|
||||
ARTIFACT,
|
||||
DependencyVerificationXmlTags.COMPONENT
|
||||
)
|
||||
}
|
||||
|
||||
override fun endElement(uri: String, localName: String, qName: String) {
|
||||
when (qName) {
|
||||
DependencyVerificationXmlTags.CONFIG -> inConfiguration =
|
||||
false
|
||||
|
||||
VERIFY_METADATA -> inVerifyMetadata =
|
||||
false
|
||||
|
||||
VERIFY_SIGNATURES -> inVerifySignatures =
|
||||
false
|
||||
|
||||
DependencyVerificationXmlTags.VERIFICATION_METADATA -> inMetadata =
|
||||
false
|
||||
|
||||
DependencyVerificationXmlTags.COMPONENTS -> inComponents =
|
||||
false
|
||||
|
||||
DependencyVerificationXmlTags.COMPONENT -> currentComponent =
|
||||
null
|
||||
|
||||
TRUSTED_ARTIFACTS -> inTrustedArtifacts =
|
||||
false
|
||||
|
||||
TRUSTED_KEYS -> inTrustedKeys =
|
||||
false
|
||||
|
||||
TRUSTED_KEY -> {
|
||||
inTrustedKey = false
|
||||
currentTrustedKey = null
|
||||
}
|
||||
|
||||
KEY_SERVERS -> inKeyServers =
|
||||
false
|
||||
|
||||
ARTIFACT -> {
|
||||
currentArtifact = null
|
||||
currentChecksum = null
|
||||
}
|
||||
|
||||
IGNORED_KEYS -> inIgnoredKeys =
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun createArtifactId(attributes: Attributes): ModuleComponentArtifactIdentifier {
|
||||
return DefaultModuleComponentArtifactIdentifier(
|
||||
currentComponent!!,
|
||||
getAttribute(
|
||||
attributes,
|
||||
DependencyVerificationXmlTags.NAME
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun createComponentId(attributes: Attributes): ModuleComponentIdentifier {
|
||||
return DefaultModuleComponentIdentifier(
|
||||
createModuleId(attributes),
|
||||
getAttribute(
|
||||
attributes,
|
||||
DependencyVerificationXmlTags.VERSION
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun createModuleId(attributes: Attributes): ModuleIdentifier {
|
||||
return DefaultModuleIdentifier(
|
||||
getAttribute(
|
||||
attributes,
|
||||
DependencyVerificationXmlTags.GROUP
|
||||
),
|
||||
getAttribute(
|
||||
attributes,
|
||||
DependencyVerificationXmlTags.NAME
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getAttribute(attributes: Attributes, name: String): String {
|
||||
val value = attributes.getValue(name)
|
||||
assertContext(
|
||||
value != null,
|
||||
"Missing attribute: $name"
|
||||
)
|
||||
return value.intern()
|
||||
}
|
||||
|
||||
private fun getNullableAttribute(attributes: Attributes, name: String): String? {
|
||||
val value = attributes.getValue(name) ?: return null
|
||||
return value.intern()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun assertContext(test: Boolean, innerTag: String, outerTag: String) {
|
||||
assertContext(
|
||||
test,
|
||||
"<$innerTag> must be found under the <$outerTag> tag"
|
||||
)
|
||||
}
|
||||
|
||||
private fun assertContext(test: Boolean, message: String) {
|
||||
if (!test) {
|
||||
throw IllegalStateException("Invalid dependency verification metadata file: $message")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.nixos.gradle2nix.metadata
|
||||
|
||||
import org.nixos.gradle2nix.dependency.ModuleComponentIdentifier
|
||||
|
||||
class DependencyVerifier internal constructor(
|
||||
val verificationMetadata: Map<ModuleComponentIdentifier, ComponentVerificationMetadata>,
|
||||
val configuration: DependencyVerificationConfiguration,
|
||||
)
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright 2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.nixos.gradle2nix.metadata
|
||||
|
||||
import org.nixos.gradle2nix.dependency.ModuleComponentArtifactIdentifier
|
||||
import org.nixos.gradle2nix.dependency.ModuleComponentIdentifier
|
||||
|
||||
class DependencyVerifierBuilder {
|
||||
private val byComponent: MutableMap<ModuleComponentIdentifier, ComponentVerificationsBuilder> = mutableMapOf()
|
||||
private val trustedArtifacts: MutableList<DependencyVerificationConfiguration.TrustCoordinates> = mutableListOf()
|
||||
|
||||
fun addChecksum(
|
||||
artifact: ModuleComponentArtifactIdentifier,
|
||||
kind: ChecksumKind,
|
||||
value: String,
|
||||
origin: String?,
|
||||
reason: String?
|
||||
) {
|
||||
val componentIdentifier: ModuleComponentIdentifier = artifact.componentIdentifier
|
||||
byComponent.getOrPut(componentIdentifier) {
|
||||
ComponentVerificationsBuilder(componentIdentifier)
|
||||
}.addChecksum(artifact, kind, value, origin, reason)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun addTrustedArtifact(
|
||||
group: String?,
|
||||
name: String?,
|
||||
version: String?,
|
||||
fileName: String?,
|
||||
regex: Boolean,
|
||||
reason: String? = null
|
||||
) {
|
||||
validateUserInput(group, name, version, fileName)
|
||||
trustedArtifacts.add(DependencyVerificationConfiguration.TrustCoordinates(group, name, version, fileName, regex, reason))
|
||||
}
|
||||
|
||||
private fun validateUserInput(
|
||||
group: String?,
|
||||
name: String?,
|
||||
version: String?,
|
||||
fileName: String?
|
||||
) {
|
||||
// because this can be called from parsing XML, we need to perform additional verification
|
||||
if (group == null && name == null && version == null && fileName == null) {
|
||||
throw IllegalStateException("A trusted artifact must have at least one of group, name, version or file name not null")
|
||||
}
|
||||
}
|
||||
|
||||
fun build(): DependencyVerifier {
|
||||
return DependencyVerifier(
|
||||
byComponent
|
||||
.toSortedMap(
|
||||
compareBy<ModuleComponentIdentifier> { it.group }
|
||||
.thenBy { it.module }
|
||||
.thenBy { it.version }
|
||||
)
|
||||
.mapValues { it.value.build() },
|
||||
DependencyVerificationConfiguration(trustedArtifacts),
|
||||
)
|
||||
}
|
||||
|
||||
private class ComponentVerificationsBuilder(private val component: ModuleComponentIdentifier) {
|
||||
private val byArtifact: MutableMap<String, ArtifactVerificationBuilder> = mutableMapOf()
|
||||
|
||||
fun addChecksum(
|
||||
artifact: ModuleComponentArtifactIdentifier,
|
||||
kind: ChecksumKind,
|
||||
value: String,
|
||||
origin: String?,
|
||||
reason: String?
|
||||
) {
|
||||
byArtifact.computeIfAbsent(artifact.fileName) { ArtifactVerificationBuilder() }
|
||||
.addChecksum(kind, value, origin, reason)
|
||||
}
|
||||
|
||||
fun build(): ComponentVerificationMetadata {
|
||||
return ComponentVerificationMetadata(
|
||||
component,
|
||||
byArtifact
|
||||
.map { ArtifactVerificationMetadata(it.key, it.value.buildChecksums()) }
|
||||
.sortedBy { it.artifactName }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protected class ArtifactVerificationBuilder {
|
||||
private val builder: MutableMap<ChecksumKind, ChecksumBuilder> = mutableMapOf()
|
||||
|
||||
fun addChecksum(kind: ChecksumKind, value: String, origin: String?, reason: String?) {
|
||||
val builder = builder.getOrPut(kind) {
|
||||
ChecksumBuilder(kind)
|
||||
}
|
||||
builder.addChecksum(value)
|
||||
if (origin != null) {
|
||||
builder.withOrigin(origin)
|
||||
}
|
||||
if (reason != null) {
|
||||
builder.withReason(reason)
|
||||
}
|
||||
}
|
||||
|
||||
fun buildChecksums(): List<Checksum> {
|
||||
return builder.values
|
||||
.map(ChecksumBuilder::build)
|
||||
.sortedBy { it.kind }
|
||||
}
|
||||
}
|
||||
|
||||
private class ChecksumBuilder(private val kind: ChecksumKind) {
|
||||
private var value: String? = null
|
||||
private var origin: String? = null
|
||||
private var reason: String? = null
|
||||
private var alternatives: MutableSet<String> = mutableSetOf()
|
||||
|
||||
/**
|
||||
* Sets the origin, if not set already. This is
|
||||
* mostly used for automatic generation of checksums
|
||||
*/
|
||||
fun withOrigin(origin: String?) {
|
||||
this.origin = this.origin ?: origin
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the reason, if not set already.
|
||||
*/
|
||||
fun withReason(reason: String?) {
|
||||
this.reason = this.reason ?: reason
|
||||
}
|
||||
|
||||
fun addChecksum(checksum: String) {
|
||||
if (value == null) {
|
||||
value = checksum
|
||||
} else if (value != checksum) {
|
||||
alternatives.add(checksum)
|
||||
}
|
||||
}
|
||||
|
||||
fun build(): Checksum {
|
||||
return Checksum(
|
||||
kind,
|
||||
checkNotNull(value) { "Checksum is null" },
|
||||
alternatives,
|
||||
origin,
|
||||
reason
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package org.nixos.gradle2nix
|
||||
|
||||
import org.spekframework.spek2.Spek
|
||||
import org.spekframework.spek2.style.specification.describe
|
||||
import strikt.api.expectThat
|
||||
import strikt.assertions.containsKey
|
||||
|
||||
object BuildSrcTest : Spek({
|
||||
fixture("buildsrc/plugin-in-buildsrc/kotlin")
|
||||
val fixture: Fixture by memoized()
|
||||
|
||||
describe("project with plugin in buildSrc") {
|
||||
fixture.run()
|
||||
|
||||
it("should include buildSrc in gradle env", timeout = 0) {
|
||||
expectThat(fixture.env()).containsKey("buildSrc")
|
||||
}
|
||||
}
|
||||
})
|
||||
40
app/src/test/kotlin/org/nixos/gradle2nix/GoldenTest.kt
Normal file
40
app/src/test/kotlin/org/nixos/gradle2nix/GoldenTest.kt
Normal file
@@ -0,0 +1,40 @@
|
||||
package org.nixos.gradle2nix
|
||||
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
|
||||
class GoldenTest : FunSpec({
|
||||
context("basic") {
|
||||
golden("basic/basic-java-project")
|
||||
golden("basic/basic-kotlin-project")
|
||||
}
|
||||
context("buildsrc") {
|
||||
golden("buildsrc/plugin-in-buildsrc")
|
||||
}
|
||||
context("dependency") {
|
||||
golden("dependency/classifier")
|
||||
golden("dependency/maven-bom")
|
||||
golden("dependency/snapshot")
|
||||
golden("dependency/snapshot-dynamic")
|
||||
golden("dependency/snapshot-redirect")
|
||||
}
|
||||
context("integration") {
|
||||
golden("integration/settings-buildscript")
|
||||
}
|
||||
context("ivy") {
|
||||
golden("ivy/basic")
|
||||
}
|
||||
context("plugin") {
|
||||
golden("plugin/resolves-from-default-repo")
|
||||
}
|
||||
context("s3") {
|
||||
golden("s3/maven")
|
||||
golden("s3/maven-snapshot")
|
||||
}
|
||||
context("settings") {
|
||||
golden("settings/buildscript")
|
||||
golden("settings/dependency-resolution-management")
|
||||
}
|
||||
context("subprojects") {
|
||||
golden("subprojects/multi-module")
|
||||
}
|
||||
})
|
||||
@@ -1,60 +1,126 @@
|
||||
package org.nixos.gradle2nix
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import org.spekframework.spek2.dsl.Root
|
||||
import strikt.api.expectThat
|
||||
import strikt.assertions.exists
|
||||
import strikt.assertions.isNotNull
|
||||
import strikt.assertions.toPath
|
||||
import io.kotest.assertions.fail
|
||||
import io.kotest.common.ExperimentalKotest
|
||||
import io.kotest.common.KotestInternal
|
||||
import io.kotest.core.names.TestName
|
||||
import io.kotest.core.source.sourceRef
|
||||
import io.kotest.core.spec.style.scopes.ContainerScope
|
||||
import io.kotest.core.spec.style.scopes.RootScope
|
||||
import io.kotest.core.test.NestedTest
|
||||
import io.kotest.core.test.TestScope
|
||||
import io.kotest.core.test.TestType
|
||||
import io.kotest.extensions.system.withEnvironment
|
||||
import io.kotest.matchers.equals.beEqual
|
||||
import io.kotest.matchers.equals.shouldBeEqual
|
||||
import io.kotest.matchers.file.shouldBeAFile
|
||||
import io.kotest.matchers.paths.shouldBeAFile
|
||||
import io.kotest.matchers.should
|
||||
import java.io.File
|
||||
import java.io.FileFilter
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.ExperimentalPathApi
|
||||
import kotlin.io.path.createTempDirectory
|
||||
import kotlin.io.path.inputStream
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import kotlinx.serialization.json.encodeToStream
|
||||
import okio.use
|
||||
|
||||
private val moshi = Moshi.Builder().build()
|
||||
private val app = Gradle2Nix()
|
||||
|
||||
class Fixture(val project: Path) {
|
||||
private val app = Main()
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
private val json = Json {
|
||||
prettyPrint = true
|
||||
prettyPrintIndent = " "
|
||||
}
|
||||
|
||||
fun run(vararg args: String) {
|
||||
app.main(args.toList() + project.toString())
|
||||
@OptIn(ExperimentalKotest::class, ExperimentalSerializationApi::class, KotestInternal::class)
|
||||
suspend fun TestScope.fixture(
|
||||
project: String,
|
||||
vararg args: String,
|
||||
test: suspend TestScope.(Map<String, Map<String, Artifact>>) -> Unit
|
||||
) {
|
||||
val tmp = Paths.get("build/tmp/gradle2nix").apply { toFile().mkdirs() }
|
||||
val baseDir = Paths.get("../fixtures", project).toFile()
|
||||
val children = baseDir.listFiles(FileFilter { it.isDirectory && (it.name == "groovy" || it.name == "kotlin") })
|
||||
?.toList()
|
||||
val cases = if (children.isNullOrEmpty()) {
|
||||
listOf(project to baseDir)
|
||||
} else {
|
||||
children.map { "$project.${it.name}" to it }
|
||||
}
|
||||
for (case in cases) {
|
||||
registerTestCase(
|
||||
NestedTest(
|
||||
name = TestName(case.first),
|
||||
disabled = false,
|
||||
config = null,
|
||||
type = TestType.Dynamic,
|
||||
source = sourceRef()
|
||||
) {
|
||||
var dirName = case.second.toString().replace("/", ".")
|
||||
while (dirName.startsWith(".")) dirName = dirName.removePrefix(".")
|
||||
while (dirName.endsWith(".")) dirName = dirName.removeSuffix(".")
|
||||
|
||||
fun env(): Map<String, NixGradleEnv> {
|
||||
val file = (app.outDir ?: project.toFile()).resolve("${app.envFile}.json")
|
||||
expectThat(file).toPath().exists()
|
||||
val env = file.source().buffer().use { source ->
|
||||
moshi
|
||||
.adapter<Map<String, NixGradleEnv>>(
|
||||
Types.newParameterizedType(Map::class.java, String::class.java, NixGradleEnv::class.java)
|
||||
).fromJson(source)
|
||||
}
|
||||
expectThat(env).isNotNull()
|
||||
return env!!
|
||||
val tempDir = File(tmp.toFile(), dirName)
|
||||
tempDir.deleteRecursively()
|
||||
case.second.copyRecursively(tempDir)
|
||||
|
||||
if (!tempDir.resolve("settings.gradle").exists() && !tempDir.resolve("settings.gradle.kts").exists()) {
|
||||
Files.createFile(tempDir.resolve("settings.gradle").toPath())
|
||||
}
|
||||
app.main(listOf("-d", tempDir.toString()) + args.withM2())
|
||||
val file = tempDir.resolve("${app.envFile}.json")
|
||||
file.shouldBeAFile()
|
||||
val env: Map<String, Map<String, Artifact>> = file.inputStream().buffered().use { input ->
|
||||
Json.decodeFromStream(input)
|
||||
}
|
||||
test(env)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
fun Root.fixture(name: String) {
|
||||
val fixture by memoized(
|
||||
factory = {
|
||||
val url = checkNotNull(Thread.currentThread().contextClassLoader.getResource(name)?.toURI()) {
|
||||
"$name: No test fixture found"
|
||||
val updateGolden = System.getProperty("org.nixos.gradle2nix.update-golden") != null
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
suspend fun TestScope.golden(
|
||||
project: String,
|
||||
vararg args: String,
|
||||
) {
|
||||
fixture(project, *args) { env ->
|
||||
val filename = "${testCase.name.testName}.json"
|
||||
val goldenFile = File("../fixtures/golden/$filename")
|
||||
if (updateGolden) {
|
||||
goldenFile.parentFile.mkdirs()
|
||||
goldenFile.outputStream().buffered().use { output ->
|
||||
json.encodeToStream(env, output)
|
||||
}
|
||||
val fixtureRoot = Paths.get(url)
|
||||
val dest = createTempDirectory("gradle2nix")
|
||||
val src = checkNotNull(fixtureRoot.takeIf { Files.exists(it) }) {
|
||||
"$name: Test fixture not found: $fixtureRoot"
|
||||
} else {
|
||||
if (!goldenFile.exists()) {
|
||||
fail("Golden file '$filename' doesn't exist. Run with --update-golden to generate.")
|
||||
}
|
||||
src.toFile().copyRecursively(dest.toFile())
|
||||
Fixture(dest)
|
||||
},
|
||||
destructor = {
|
||||
it.project.toFile().deleteRecursively()
|
||||
val goldenData: Map<String, Map<String, Artifact>> = try {
|
||||
goldenFile.inputStream().buffered().use { input ->
|
||||
json.decodeFromStream(input)
|
||||
}
|
||||
} catch (e: SerializationException) {
|
||||
fail("Failed to load golden data from '$filename'. Run with --update-golden to regenerate.")
|
||||
}
|
||||
env should beEqual(goldenData)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val m2 = System.getProperty("org.nixos.gradle2nix.m2")
|
||||
|
||||
private fun Array<out String>.withM2(): List<String> {
|
||||
val args = toMutableList()
|
||||
if (args.indexOf("--") < 0) args.add("--")
|
||||
args.add("-Dorg.nixos.gradle2nix.m2=$m2")
|
||||
return args
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user