GradleUtils/src/dorkbox/gradle/StaticMethodsAndTools.kt

376 lines
15 KiB
Kotlin

package dorkbox.gradle
import org.gradle.api.GradleException
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.jvm.tasks.Jar
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions
import org.jetbrains.kotlin.gradle.plugin.KotlinBasePluginWrapper
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.File
import java.util.*
import kotlin.reflect.KMutableProperty
import kotlin.reflect.KProperty
import kotlin.reflect.full.declaredMemberFunctions
import kotlin.reflect.full.declaredMemberProperties
open class StaticMethodsAndTools(private val project: Project) {
val isUnix = org.gradle.internal.os.OperatingSystem.current().isUnix
val isLinux = org.gradle.internal.os.OperatingSystem.current().isLinux
val isMac = org.gradle.internal.os.OperatingSystem.current().isMacOsX
val isWindows = org.gradle.internal.os.OperatingSystem.current().isWindows
private var fixedSWT = false
/**
* Maps the property (key/value) pairs of a property file onto the specified target object. Also maps fields in the targetObject to the
* project, if have the same name relationship (ie: field name is "version", project method is "setVersion")
*/
fun load(propertyFile: String, targetObject: Any) {
val kClass = targetObject::class
val propsFile = File(propertyFile).normalize()
if (propsFile.canRead()) {
println("\tLoading custom property data from: [$propsFile]")
val props = Properties()
propsFile.inputStream().use {
props.load(it)
}
val extraProperties = kClass.declaredMemberProperties.filterIsInstance<KMutableProperty<String>>()
val assignedExtraProperties = kClass.declaredMemberProperties.filterIsInstance<KProperty<String>>()
// project functions that can be called for setting properties
val propertyFunctions = Project::class.declaredMemberFunctions.filter { it.parameters.size == 2 }
// THREE possibilities for property registration or assignment
// 1) we have MANUALLY defined this property (via the configuration object)
// 1) gradleUtil properties loaded first
// -> gradleUtil's adds a function that everyone else (plugin/task) can call to get values from properties
// 2) gradleUtil properties loaded last
// -> others add a function that gradleUtil's call to set values from properties
// get the module loaded registration functions (if they exist)
val loaderFunctions: ArrayList<Plugin<Pair<String, String>>>?
if (project.extensions.extraProperties.has("property_loader_functions")) {
@Suppress("UNCHECKED_CAST")
loaderFunctions = project.extensions.extraProperties["property_loader_functions"] as ArrayList<Plugin<Pair<String, String>>>?
} else {
loaderFunctions = null
}
props.forEach { (k, v) -> run {
val key = k as String
val value = v as String
val member = extraProperties.find { it.name == key }
// if we have a property name in our object, we set it.
member?.setter?.call(kClass.objectInstance, value)
// if we have a property name in our PROJECT, we set it
// project functions that can be called for setting properties
val setterName = "set${key.capitalize()}"
propertyFunctions.find { prop -> prop.name == setterName }?.call(project, value)
// assign this as an "extra property"
project.extensions.extraProperties.set(k, v)
// apply this property to whatever loader functions have been dynamically applied
val pair = Pair(key, value)
loaderFunctions?.forEach {
it.apply(pair)
}
}}
// assign target fields to our project (if our project has matching setters)
assignedExtraProperties.forEach {
val propertyName = it.name
val setterName = "set${propertyName.capitalize()}"
val projectMethod = propertyFunctions.find { prop -> prop.name == setterName }
if (projectMethod != null) {
if (it.getter.property.isConst) {
projectMethod.call(project, it.getter.call())
} else {
projectMethod.call(project, it.getter.call(kClass.objectInstance))
}
}
}
}
}
/**
* Validates the minimum version of gradle supported
*/
fun minVersion(version: String) {
val compared = org.gradle.util.GradleVersion.current().compareTo(org.gradle.util.GradleVersion.version(version))
if (compared == -1) {
throw GradleException("This project requires Gradle $version or higher.")
}
}
/**
* Validates the maximum version of gradle supported
*/
fun maxVersion(version: String) {
val compared = org.gradle.util.GradleVersion.current().compareTo(org.gradle.util.GradleVersion.version(version))
if (compared == 1) {
throw GradleException("This project requires Gradle $version or lower.")
}
}
/**
* Resolves all child dependencies of the project
*
* THIS MUST BE IN "afterEvaluate" or run from a specific task.
*/
fun resolveDependencies(): List<DependencyScanner.Dependency> {
val projectDependencies = mutableListOf<DependencyScanner.Dependency>()
val existingNames = mutableSetOf<String>()
DependencyScanner.scan(project, "compileClasspath", projectDependencies, existingNames)
DependencyScanner.scan(project, "runtimeClasspath", projectDependencies, existingNames)
return projectDependencies
}
/**
* Resolves all child compile dependencies of the project
*
* THIS MUST BE IN "afterEvaluate" or run from a specific task.
*/
fun resolveCompileDependencies(): List<DependencyScanner.Dependency> {
val projectDependencies = mutableListOf<DependencyScanner.Dependency>()
val existingNames = mutableSetOf<String>()
DependencyScanner.scan(project, "compileClasspath", projectDependencies, existingNames)
return projectDependencies
}
/**
* Resolves all child compile dependencies of the project
*
* THIS MUST BE IN "afterEvaluate" or run from a specific task.
*/
fun resolveRuntimeDependencies(): List<DependencyScanner.Dependency> {
val projectDependencies = mutableListOf<DependencyScanner.Dependency>()
val existingNames = mutableSetOf<String>()
DependencyScanner.scan(project, "runtimeClasspath", projectDependencies, existingNames)
return projectDependencies
}
/**
* Fix the compiled output from intellij to be SEPARATE from gradle.
*/
fun fixIntellijPaths(location: String = "${project.buildDir}/classes-intellij") {
project.allprojects.forEach { project ->
// https://discuss.gradle.org/t/can-a-plugin-itself-add-buildscript-dependencies-and-then-apply-a-plugin/25039/4
apply(project, "idea")
// put idea in it's place! Not having this causes SO MANY PROBLEMS when building modules
idea(project) {
// https://youtrack.jetbrains.com/issue/IDEA-175172
module {
val mainDir = File(location)
it.outputDir = mainDir
it.testOutputDir = mainDir
// by default, we ALWAYS want sources. If you have sources, you don't need javadoc (since the sources have them in it already)
it.isDownloadJavadoc = false
it.isDownloadSources = true
}
}
// this has the side-effect of NOT creating the gradle directories....
// make sure that the source set directories all exist. THIS SHOULD NOT BE A PROBLEM!
project.afterEvaluate { prj ->
prj.allprojects.forEach { proj ->
val javaPlugin: JavaPluginConvention = proj.convention.getPlugin(JavaPluginConvention::class.java)
val sourceSets = javaPlugin.sourceSets
sourceSets.forEach { set ->
set.output.classesDirs.forEach { dir ->
if (!dir.exists()) {
dir.mkdirs()
}
}
}
}
}
}
}
/**
* Configure a default resolution strategy. While not necessary, this is used for enforcing sane project builds
*/
fun defaultResolutionStrategy() {
project.allprojects.forEach { project ->
project.configurations.forEach { config ->
config.resolutionStrategy {
it.apply {
// fail eagerly on version conflict (includes transitive dependencies)
// e.g. multiple different versions of the same dependency (group and name are equal)
failOnVersionConflict()
// if there is a version we specified, USE THAT VERSION (over transitive versions)
preferProjectModules()
// cache dynamic versions for 10 minutes
cacheDynamicVersionsFor(10 * 60, "seconds")
// don't cache changing modules at all
cacheChangingModulesFor(0, "seconds")
}
}
}
}
}
/**
* Basic, default compile configurations
*/
fun compileConfiguration(javaVersion: JavaVersion, kotlinActions: (KotlinJvmOptions) -> Unit = {}) {
val javaVer = javaVersion.toString()
val kotlinVer: String = try {
val kot = project.plugins.findPlugin("org.jetbrains.kotlin.jvm") as org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapper?
val version = kot?.kotlinPluginVersion ?: "1.3.0"
// we ONLY care about the major.minor
val secondDot = version.indexOf('.', version.indexOf('.')+1)
version.substring(0, secondDot)
} catch (e: Exception) {
// in case we cannot parse it from the plugin, provide a reasonable default
"1.3"
}
project.allprojects.forEach { project ->
project.tasks.withType(JavaCompile::class.java) { task ->
task.doFirst {
println("\tCompiling classes to Java $javaVersion")
}
task.options.encoding = "UTF-8"
// -Xlint:deprecation
task.options.isDeprecation = true
// -Xlint:unchecked
task.options.compilerArgs.add("-Xlint:unchecked")
task.sourceCompatibility = javaVer
task.targetCompatibility = javaVer
}
project.tasks.withType(Jar::class.java) {
it.duplicatesStrategy = DuplicatesStrategy.FAIL
}
project.tasks.withType(KotlinCompile::class.java) { task ->
task.doFirst {
println("\tCompiling classes to Kotlin ${task.kotlinOptions.languageVersion}, Java ${task.kotlinOptions.jvmTarget}")
}
task.sourceCompatibility = javaVer
task.targetCompatibility = javaVer
task.kotlinOptions.jvmTarget = javaVer
// default is whatever the version is that we are running, or 1.3 if we cannot figure it out
task.kotlinOptions.apiVersion = kotlinVer
task.kotlinOptions.languageVersion = kotlinVer
// see: https://kotlinlang.org/docs/reference/using-gradle.html
kotlinActions(task.kotlinOptions)
}
}
}
/**
* Get the SWT maven ID based on the os/arch. ALSO fix SWT maven configuration IDs
*
* This is spectacularly frustrating because there aren't "normal" releases of SWT.
*/
fun getSwtMavenId(version: String): String {
// SEE: https://repo1.maven.org/maven2/org/eclipse/platform/
// windows
// org.eclipse.swt.win32.win32.x86
// org.eclipse.swt.win32.win32.x86_64
// linux
// org.eclipse.swt.gtk.linux.x86
// org.eclipse.swt.gtk.linux.x86_64
// macoxs
// org.eclipse.swt.cocoa.macosx.x86_64
val currentOS = org.gradle.internal.os.OperatingSystem.current()
val windowingTk = when {
currentOS.isWindows -> "win32"
currentOS.isMacOsX -> "cocoa"
else -> "gtk"
}
val platform = when {
currentOS.isWindows -> "win32"
currentOS.isMacOsX -> "macosx"
else -> "linux"
}
var arch = System.getProperty("os.arch")
arch = when {
arch.matches(".*64.*".toRegex()) -> "x86_64"
else -> "x86"
}
val mavenId = "$windowingTk.$platform.$arch"
if (!fixedSWT) {
fixedSWT = true
project.allprojects.forEach { project ->
project.configurations.all { config ->
config.resolutionStrategy { strat ->
strat.dependencySubstitution { sub ->
// The maven property ${osgi.platform} is not handled by Gradle for the SWT builds
// so we replace the dependency, using the osgi platform from the project settings
sub.substitute(sub.module("org.eclipse.platform:org.eclipse.swt.\${osgi.platform}"))
.with(sub.module("org.eclipse.platform:org.eclipse.swt.$mavenId:$version"))
}
}
}
}
}
return "org.eclipse.platform:org.eclipse.swt.$mavenId:$version"
}
private fun idea(project: Project, configure: org.gradle.plugins.ide.idea.model.IdeaModel.() -> Unit): Unit =
project.extensions.configure("idea", configure)
// required to make sure the plugins are correctly applied. ONLY applying it to the project WILL NOT work.
// The plugin must also be applied to the root project
private fun apply(project: Project, id: String) {
if (project.rootProject.pluginManager.findPlugin(id) == null) {
project.rootProject.pluginManager.apply(id)
}
if (project.pluginManager.findPlugin(id) == null) {
project.pluginManager.apply(id)
}
}
}