2020-05-25 11:23:45 +02:00
/ *
2021-12-15 00:36:32 +01:00
* Copyright 2021 dorkbox , llc
2020-05-25 11:23:45 +02:00
*
* 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 dorkbox.gradlePublish
2023-09-18 03:12:29 +02:00
import io.github.gradlenexus.publishplugin.NexusPublishExtension
import io.github.gradlenexus.publishplugin.NexusRepository
import io.github.gradlenexus.publishplugin.NexusRepositoryContainer
2021-12-14 20:59:01 +01:00
import org.gradle.api.*
2023-01-17 01:48:50 +01:00
import org.gradle.api.file.DuplicatesStrategy
2020-05-25 11:23:45 +02:00
import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.publish.maven.tasks.PublishToMavenLocal
import org.gradle.api.publish.maven.tasks.PublishToMavenRepository
2023-01-17 01:48:50 +01:00
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.SourceSetContainer
import org.gradle.api.tasks.util.PatternFilterable
2020-05-25 11:23:45 +02:00
import org.gradle.jvm.tasks.Jar
2020-06-08 22:20:22 +02:00
import org.gradle.plugins.signing.SigningExtension
import org.gradle.plugins.signing.signatory.internal.pgp.InMemoryPgpSignatoryProvider
2023-01-17 01:48:50 +01:00
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
2020-06-08 22:20:22 +02:00
import java.io.File
import java.time.Duration
2021-12-15 00:36:32 +01:00
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
2020-06-02 23:06:50 +02:00
import java.util.*
2020-05-25 11:23:45 +02:00
/ * *
* For managing ( what should be common sense ) gradle tasks , such as :
* - publishing gradle projects to sonatype
* /
@Suppress ( " UnstableApiUsage " , " unused " )
class PublishPlugin : Plugin < Project > {
2020-06-02 22:44:56 +02:00
companion object {
2021-12-15 00:36:32 +01:00
val DTF = DateTimeFormatter . ofPattern ( " E MMM HH:mm:ss 'UTC' yyyy " ) . withZone ( ZoneOffset . UTC )
2020-06-02 22:44:56 +02:00
init {
// To fix maven+gradle moronic incompatibilities: https://github.com/gradle/gradle/issues/11308
System . setProperty ( " org.gradle.internal.publish.checksums.insecure " , " true " )
}
2023-01-17 01:48:50 +01:00
/ * *
* If the kotlin plugin is applied , and there is a compileKotlin task .. Then kotlin is enabled
* NOTE : This can ONLY be called from a task , it cannot be called globally !
* /
fun hasKotlin ( project : Project , debug : Boolean = false ) : Boolean {
try {
// check if plugin is available
project . plugins . findPlugin ( " org.jetbrains.kotlin.jvm " ) ?: return false
if ( debug ) println ( " \t Has kotlin plugin " )
// this will check if the task exists, and throw an exception if it does not or return false
project . tasks . named ( " compileKotlin " , KotlinCompile :: class . java ) . orNull ?: return false
if ( debug ) println ( " \t Has compile kotlin task " )
// check to see if we have any kotlin file
val sourceSets = project . extensions . getByName ( " sourceSets " ) as SourceSetContainer
val main = sourceSets . getByName ( " main " )
val kotlin = project . extensions . getByType ( org . jetbrains . kotlin . gradle . dsl . KotlinJvmProjectExtension :: class . java ) . sourceSets . getByName ( " main " ) . kotlin
if ( debug ) {
println ( " \t main dirs: ${main.java.srcDirs} " )
println ( " \t kotlin dirs: ${kotlin.srcDirs} " )
project . buildFile . parentFile . walkTopDown ( ) . filter { it . extension == " kt " } . forEach {
println ( " \t \t $it " )
}
}
val files = main . java . srcDirs + kotlin . srcDirs
files . forEach { srcDir ->
val kotlinFile = srcDir . walkTopDown ( ) . find { it . extension == " kt " }
if ( kotlinFile ?. exists ( ) == true ) {
if ( debug ) println ( " \t Has kotlin file: $kotlinFile " )
return true
}
}
} catch ( e : Exception ) {
if ( debug ) e . printStackTrace ( )
}
return false
}
2020-06-02 22:44:56 +02:00
}
2020-05-25 11:23:45 +02:00
private lateinit var project : Project
2021-12-15 00:36:32 +01:00
@Volatile
private var hasMavenOutput = false
2023-01-17 01:48:50 +01:00
// this is lazy, because it MUST be run from a task!
private val hasKotlin : Boolean by lazy { hasKotlin ( project ) }
2021-12-14 20:59:01 +01:00
@Suppress ( " ObjectLiteralToLambda " )
2020-05-25 11:23:45 +02:00
override fun apply ( project : Project ) {
this . project = project
// https://discuss.gradle.org/t/can-a-plugin-itself-add-buildscript-dependencies-and-then-apply-a-plugin/25039/4
apply ( " java " )
apply ( " maven-publish " )
apply ( " signing " )
2023-09-18 03:12:29 +02:00
apply ( " io.github.gradle-nexus.publish-plugin " )
2020-05-25 11:23:45 +02:00
// Create the Plugin extension object (for users to configure publishing).
val config = project . extensions . create ( " publishToSonatype " , PublishToSonatype :: class . java , project )
2023-01-17 01:48:50 +01:00
val sourceJar = project . tasks . create ( " sourceJar " , Jar :: class . java ) . apply {
description = " Creates a JAR that contains the source code. "
archiveClassifier . set ( " sources " )
mustRunAfter ( project . tasks . getByName ( " jar " ) )
duplicatesStrategy = DuplicatesStrategy . FAIL
}
val javaDocJar = project . tasks . create ( " javaDocJar " , Jar :: class . java ) . apply {
description = " Creates a JAR that contains the javadocs. "
// nothing in javadocs. sources is all we care about
archiveClassifier . set ( " javadoc " )
mustRunAfter ( project . tasks . getByName ( " jar " ) )
}
2020-05-25 11:23:45 +02:00
// specific configuration later in after evaluate!!
nexusPublishing {
packageGroup . set ( project . provider { config . groupId } )
2020-06-08 22:20:22 +02:00
clientTimeout . set ( project . provider { config . httpTimeout } )
connectTimeout . set ( project . provider { config . httpTimeout } )
2020-05-25 11:23:45 +02:00
repositories ( Action < NexusRepositoryContainer > {
it . sonatype ( Action < NexusRepository > { repo ->
2020-06-08 22:20:22 +02:00
assignFromProp ( " sonatypeUserName " , config . sonatype . userName ) { repo . username . set ( project . provider { it } ) }
assignFromProp ( " sonatypePassword " , config . sonatype . password ) { repo . password . set ( project . provider { it } ) }
2020-05-25 11:23:45 +02:00
} )
} )
}
publishing {
publications { pub ->
val mavPub = pub . maybeCreate ( " maven " , MavenPublication :: class . java )
mavPub . from ( project . components . getByName ( " java " ) )
2023-01-17 01:48:50 +01:00
2020-05-25 11:23:45 +02:00
// create the pom
mavPub . pom { pom ->
pom . organization {
}
pom . issueManagement {
2020-06-08 22:20:22 +02:00
val sign = project . extensions . getByName ( " signing " ) as SigningExtension
// check what the signatory is. if it's InMemoryPgpSignatoryProvider, then we ALREADY configured it!
if ( sign . signatory !is InMemoryPgpSignatoryProvider ) {
// we haven't configured it yet AND we don't know which value is set first!
// setup the sonatype PRIVATE KEY information
2022-11-14 23:30:22 +01:00
assignFromProp ( " sonatypePrivateKeyFile " , " " ) { fileName ->
project . extensions . extraProperties [ " sonatypePrivateKeyFile " ] = fileName
2020-06-08 22:20:22 +02:00
if ( project . extensions . extraProperties . has ( " sonatypePrivateKeyPassword " ) ) {
2022-11-14 23:30:22 +01:00
val password = project . extensions . extraProperties [ " sonatypePrivateKeyPassword " ] as String
val fileText = File ( fileName ) . readText ( )
if ( fileText . isNotEmpty ( ) ) {
sign . apply {
useInMemoryPgpKeys ( fileText , password )
}
2020-06-08 22:20:22 +02:00
}
}
}
2022-11-14 23:30:22 +01:00
assignFromProp ( " sonatypePrivateKeyPassword " , " " ) { password ->
project . extensions . extraProperties [ " sonatypePrivateKeyPassword " ] = password
2020-06-08 22:20:22 +02:00
if ( project . extensions . extraProperties . has ( " sonatypePrivateKeyFile " ) ) {
2022-11-14 23:30:22 +01:00
val fileName = project . extensions . extraProperties [ " sonatypePrivateKeyFile " ] as String
if ( fileName . isNotEmpty ( ) ) {
val fileText = File ( fileName ) . readText ( )
if ( fileText . isNotEmpty ( ) ) {
sign . apply {
useInMemoryPgpKeys ( fileText , password )
}
}
2020-06-08 22:20:22 +02:00
}
}
}
}
2020-05-25 11:23:45 +02:00
}
pom . scm {
}
pom . developers {
}
}
2023-01-17 01:48:50 +01:00
mavPub . artifact ( sourceJar )
2020-05-25 11:23:45 +02:00
2023-01-17 01:48:50 +01:00
mavPub . artifact ( javaDocJar )
2020-05-25 11:23:45 +02:00
}
}
2021-12-14 20:59:01 +01:00
project . tasks . create ( " getSonatypeUrl " ) . apply {
outputs . upToDateWhen { false }
outputs . cacheIf { false }
group = " publish and release "
this . doLast ( object : Action < Task > {
override fun execute ( task : Task ) {
val url = " https://oss.sonatype.org/content/repositories/releases/ "
val projectName = config . groupId . replace ( '.' , '/' )
// output the release URL in the console
println ( " \t Sonatype URL: $url $projectName / ${config.name} / ${config.version} / " )
}
} )
}
2023-09-18 03:12:29 +02:00
2021-01-26 23:51:14 +01:00
project . tasks . getByName ( " publishToMavenLocal " ) . apply {
2021-12-14 20:59:01 +01:00
outputs . upToDateWhen { false }
outputs . cacheIf { false }
2021-01-26 23:51:14 +01:00
group = " publish and release "
}
2021-12-14 20:59:01 +01:00
project . tasks . create ( " publishToSonatypeAndRelease " ) . apply {
outputs . upToDateWhen { false }
outputs . cacheIf { false }
2020-06-02 22:44:56 +02:00
group = " publish and release "
2021-12-14 20:59:01 +01:00
description = " Publish and Release this project to the Sonatype Maven repository "
2020-05-25 11:23:45 +02:00
2023-09-18 03:12:29 +02:00
dependsOn ( " publishToMavenLocal " , " publishToSonatype " , " closeAndReleaseSonatypeStagingRepository " )
2020-05-25 11:23:45 +02:00
}
2021-12-15 00:36:32 +01:00
2020-05-25 11:23:45 +02:00
project . tasks . withType < PublishToMavenLocal > {
2020-08-08 00:10:43 +02:00
doFirst {
2023-11-27 13:55:29 +01:00
// prune off the "file:"
val localMavenRepo = project . repositories . mavenLocal ( ) . url . toString ( ) . replaceFirst ( " file: " , " " )
val projectName = config . groupId . replace ( '.' , '/' )
// output the release URL in the console
val mavenLocation = " $localMavenRepo $projectName / ${config.name} / ${config.version} / "
2023-11-27 17:07:56 +01:00
// clean-out the repo!!
File ( mavenLocation ) . deleteRecursively ( )
2020-08-08 00:10:43 +02:00
println ( " \t Publishing ' ${publication.groupId} : ${publication.artifactId} : ${publication.version} ' to Maven Local " )
2023-11-27 13:55:29 +01:00
2022-11-14 23:36:40 +01:00
publication . artifacts . forEach {
2023-11-27 13:55:29 +01:00
val file = File ( " $mavenLocation / ${it.file.name} " )
println ( " \t \t $file " )
2022-11-14 23:36:40 +01:00
}
2020-08-08 00:10:43 +02:00
}
2020-05-25 11:23:45 +02:00
onlyIf {
val pub = get ( )
publication == pub . publications . getByName ( " maven " )
}
}
project . tasks . withType < PublishToMavenRepository > {
doFirst {
2020-08-08 01:00:04 +02:00
val url = " https://oss.sonatype.org/content/repositories/releases/ "
val projectName = config . groupId . replace ( '.' , '/' )
// output the release URL in the console
println ( " \t Publishing ' ${publication.groupId} : ${publication.artifactId} : ${publication.version} ' to $url $projectName / ${config.name} / ${config.version} / " )
2020-05-25 11:23:45 +02:00
}
onlyIf {
val pub = get ( )
publication == pub . publications . getByName ( " maven " ) &&
repository == pub . repositories . getByName ( " sonatype " )
}
}
// have to get the configuration extension data
// required to make sure the tasks run in the correct order
project . afterEvaluate {
2023-09-18 03:12:29 +02:00
// (when the dependencies are there) we want to ALWAYS run maven local FIRST.
project . tasks . getByName ( " publishToSonatype " ) . mustRunAfter ( project . tasks . getByName ( " publishToMavenLocal " ) )
project . tasks . getByName ( " closeAndReleaseSonatypeStagingRepository " ) . mustRunAfter ( project . tasks . getByName ( " publishToSonatype " ) )
// only add files to the PRIMARY jar if we are deploying to maven
// this is a LITTLE HACKY, but we have to modify the task graph BEFORE the task graph is calculated...
val taskNames = project . gradle . startParameter . taskNames
hasMavenOutput = taskNames . contains ( " publishToMavenLocal " ) || taskNames . contains ( " publishToSonatypeAndRelease " )
if ( hasMavenOutput ) {
project . tasks . getByName ( " jar " ) . apply {
dependsOn ( " generatePomFileForMavenPublication " )
outputs . upToDateWhen { false }
outputs . cacheIf { false }
}
2020-06-08 22:20:22 +02:00
}
2023-09-18 03:12:29 +02:00
nexusPublishing {
2023-09-25 15:15:41 +02:00
useStaging . set ( true )
2023-09-18 03:12:29 +02:00
transitionCheckOptions {
it . maxRetries . set ( config . retryLimit )
it . delayBetween . set ( config . retryDelay )
}
2020-06-08 22:20:22 +02:00
}
2023-09-18 03:12:29 +02:00
// this makes sure that we run this AFTER all the info in the project has been figured out, but before it's run (so we can still modify it)
project . tasks . getByName ( " sourceJar " ) . apply {
val task = this as Jar
// println("Configuring jar sources: ${task.name}")
val sourceSets = project . extensions . getByName ( " sourceSets " ) as SourceSetContainer
val mainSourceSet : SourceSet = sourceSets . getByName ( " main " )
if ( hasKotlin ) {
// println("Kotlin sources: ${task.name}")
// want to included java + kotlin for the sources
// kotlin stuff. Sometimes kotlin depends on java files, so the kotlin source-sets have BOTH java + kotlin.
// we want to make sure to NOT have both, as it will screw up creating the jar!
try {
val kotlin = project . extensions . getByType ( org . jetbrains . kotlin . gradle . dsl . KotlinJvmProjectExtension :: class . java ) . sourceSets . getByName ( " main " ) . kotlin
val srcDirs = kotlin . srcDirs
val kotlinFiles = kotlin . asFileTree . matching { it : PatternFilterable ->
// find out if this file (usually, just a java file) is ALSO in the java source-set.
// this is to prevent DUPLICATES in the jar, because sometimes kotlin must be .kt + .java in order to compile!
val javaFiles = mainSourceSet . java . files . map { file ->
// by definition, it MUST be one of these
val base = srcDirs . first {
// find out WHICH src dir base path it is
val path = project . layout . buildDirectory . locationOnly . get ( ) . asFile . relativeTo ( it )
path . path . isNotEmpty ( )
}
// there can be leading "../" (since it's relative. WE DO NOT WANT THAT!
val newFile = file . relativeTo ( base ) . path . replace ( " ../ " , " " )
// println("\t\tAdding: $newFile")
newFile
}
it . setExcludes ( javaFiles )
}
// kotlinFiles.forEach {
// println("\t$it")
// }
task . from ( kotlinFiles )
} catch ( ignored : Exception ) {
// maybe we don't have kotlin for the project
}
}
// kotlin is always compiled first
// println("Java sources: ${task.name}")
// mainSourceSet.java.files.forEach {
// println("\t$it")
// }
task . from ( mainSourceSet . java )
2020-06-08 22:20:22 +02:00
}
2023-09-18 03:12:29 +02:00
2020-06-08 22:20:22 +02:00
// create the sign task to sign the artifact jars before uploading
val sign = project . extensions . getByName ( " signing " ) as SigningExtension
sign . apply {
2022-11-14 23:30:01 +01:00
// only sign if we have something configured for signing!
if ( project . extensions . extraProperties . has ( " sonatypePrivateKeyFile " ) ) {
sign ( ( project . extensions . getByName ( " publishing " ) as PublishingExtension ) . publications . getByName ( " maven " ) )
}
2020-06-08 22:20:22 +02:00
}
// output how much the time-outs are
val durationString = config . httpTimeout . toString ( ) . substring ( 2 )
2021-09-14 10:22:50 +02:00
. replace ( " ( \\ d[HMS])(?! $ ) " , " $ 1 " ) . lowercase ( Locale . getDefault ( ) )
2020-06-08 22:20:22 +02:00
val fullReleaseTimeout = Duration . ofMillis ( config . retryDelay . toMillis ( ) * config . retryLimit )
val fullReleaseString = fullReleaseTimeout . toString ( ) . substring ( 2 )
2021-09-14 10:22:50 +02:00
. replace ( " ( \\ d[HMS])(?! $ ) " , " $ 1 " ) . lowercase ( Locale . getDefault ( ) )
2020-06-08 22:20:22 +02:00
2020-08-08 00:10:43 +02:00
project . tasks . findByName ( " publishToSonatype " ) ?. doFirst {
2020-08-08 00:52:04 +02:00
println ( " \t Publishing to Sonatype: ${config.groupId} : ${config.artifactId} : ${config.version} " )
2020-08-08 00:10:43 +02:00
println ( " \t \t Sonatype HTTP timeout: $durationString " )
println ( " \t \t Sonatype API timeout: $fullReleaseString " )
}
2021-12-15 00:36:32 +01:00
if ( hasMavenOutput ) {
( project . tasks . getByName ( " jar " ) as Jar ) . apply {
// we have to generate the POM file BEFORE the jar (so the outputs are available!)
val pomFileTask = project . tasks . getByName ( " generatePomFileForMavenPublication " )
val pomFile = pomFileTask . outputs . files . first ( )
val pomProps = pomFile . parentFile . resolve ( " pom.properties " )
from ( pomFile ) {
it . into ( " META-INF/maven/ ${project.group} / ${project.name} " )
it . rename { " pom.xml " }
}
from ( pomProps ) {
it . into ( " META-INF/maven/ ${project.group} / ${project.name} " )
}
this . doFirst ( object : Action < Task > {
override fun execute ( task : Task ) {
// have to write out the pom.properties file
pomProps . writeText (
" #Generated by Dorkbox \n " +
" # ${DTF.format(Instant.now())} \n " +
" version= ${config.version} \n " +
" groupId= ${config.groupId} \n " +
" artifactId= ${config.artifactId} \n " )
}
} )
}
}
2020-05-25 11:23:45 +02:00
}
project . childProjects . values . forEach {
it . pluginManager . apply ( PublishPlugin :: class . java )
}
}
// 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 ( id : String ) {
if ( project . rootProject . pluginManager . findPlugin ( id ) == null ) {
project . rootProject . pluginManager . apply ( id )
}
if ( project . pluginManager . findPlugin ( id ) == null ) {
project . pluginManager . apply ( id )
}
}
private inline fun < reified S : Any > DomainObjectCollection < in S > . withType ( noinline configuration : S . ( ) -> Unit ) =
withType ( S :: class . java , configuration )
private fun get ( ) : PublishingExtension {
return project . extensions . getByName ( " publishing " ) as PublishingExtension
}
private fun publishing ( configure : PublishingExtension . ( ) -> Unit ) : Unit =
project . extensions . configure ( " publishing " , configure )
private fun nexusPublishing ( configure : NexusPublishExtension . ( ) -> Unit ) : Unit =
project . extensions . configure ( " nexusPublishing " , configure )
2020-06-08 22:20:22 +02:00
2023-09-18 03:12:29 +02:00
@Suppress ( " UNCHECKED_CAST " )
2020-06-08 22:20:22 +02:00
private fun assignFromProp ( propertyName : String , defaultValue : String , apply : ( value : String ) -> Unit ) {
// THREE possibilities for property registration or assignment
// 1) we have MANUALLY defined this property (via the configuration object)
2022-11-14 23:36:50 +01:00
// 2) gradleUtil properties loaded first
2020-06-08 22:20:22 +02:00
// -> gradleUtil's adds a function that everyone else (plugin/task) can call to get values from properties
2022-11-14 23:36:50 +01:00
// 3) gradleUtil properties loaded last
2020-06-08 22:20:22 +02:00
// -> others add a function that gradleUtil's call to set values from properties
// 1
if ( defaultValue . isNotEmpty ( ) ) {
// println("ASSIGN DEFAULT: $defaultValue")
apply ( defaultValue )
return
}
// 2
if ( project . extensions . extraProperties . has ( propertyName ) ) {
// println("ASSIGN PROP FROM FILE: $propertyName")
apply ( project . extensions . extraProperties [ propertyName ] as String )
return
}
// 3
2020-08-17 16:16:23 +02:00
val loaderFunctions : ArrayList < Plugin < Pair < String , String > > > ?
2020-06-08 22:20:22 +02:00
if ( project . extensions . extraProperties . has ( " property_loader_functions " ) ) {
loaderFunctions = project . extensions . extraProperties [ " property_loader_functions " ] as ArrayList < Plugin < Pair < String , String > > > ?
} else {
loaderFunctions = ArrayList < Plugin < Pair < String , String > > > ( )
project . extensions . extraProperties [ " property_loader_functions " ] = loaderFunctions
}
// println("ADD LOADER FUNCTION: $propertyName")
loaderFunctions !! . add ( Plugin < Pair < String , String > > ( ) {
if ( it . first == propertyName ) {
// println("EXECUTE LOADER FUNCTION: $propertyName")
apply ( it . second )
}
} )
}
2020-05-25 11:23:45 +02:00
}