Unverified Commit 0c4350b8 authored by Alexey Tsvetkov's avatar Alexey Tsvetkov Committed by GitHub

Prepare publishing to Maven Central (#293)

* Make publications comply with Maven Central requirements

* Prepare for publishing to Maven Central

* Rename runtimeArtifactIdFor -> jvmRuntimeArtifactIdFor
parent e70b6dca
...@@ -6,8 +6,9 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile ...@@ -6,8 +6,9 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile
plugins { plugins {
kotlin("multiplatform") version "1.5.31" kotlin("multiplatform") version "1.5.31"
`maven-publish` `maven-publish`
signing
id("org.gradle.crypto.checksum") version "1.1.0" id("org.gradle.crypto.checksum") version "1.1.0"
id("de.undercouch.download") version "4.1.1" id("de.undercouch.download")
} }
val coroutinesVersion = "1.5.2" val coroutinesVersion = "1.5.2"
...@@ -958,6 +959,16 @@ afterEvaluate { ...@@ -958,6 +959,16 @@ afterEvaluate {
} }
} }
val emptySourcesJar by tasks.registering(Jar::class) {
archiveClassifier.set("sources")
}
val emptyJavadocJar by tasks.registering(Jar::class) {
archiveClassifier.set("javadoc")
}
publishing { publishing {
repositories { repositories {
configureEach { configureEach {
...@@ -981,35 +992,85 @@ publishing { ...@@ -981,35 +992,85 @@ publishing {
} }
} }
publications { publications {
val pomNameForPublication = HashMap<String, String>()
pomNameForPublication["kotlinMultiplatform"] = "Skiko MPP"
kotlin.targets.forEach {
pomNameForPublication[it.name] = "Skiko ${toTitleCase(it.name)}"
}
configureEach { configureEach {
this as MavenPublication this as MavenPublication
groupId = "org.jetbrains.skiko" groupId = "org.jetbrains.skiko"
// Necessary for publishing to Maven Central
artifact(emptyJavadocJar)
pom { pom {
name.set("Skiko")
description.set("Kotlin Skia bindings") description.set("Kotlin Skia bindings")
url.set("http://www.github.com/JetBrains/skiko")
licenses { licenses {
license { license {
name.set("The Apache License, Version 2.0") name.set("The Apache License, Version 2.0")
url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
} }
} }
val repoUrl = "https://www.github.com/JetBrains/skiko"
url.set(repoUrl)
scm {
url.set(repoUrl)
val repoConnection = "scm:git:$repoUrl.git"
connection.set(repoConnection)
developerConnection.set(repoConnection)
}
developers {
developer {
name.set("Compose Multiplatform Team")
organization.set("JetBrains")
organizationUrl.set("https://www.jetbrains.com")
}
}
} }
} }
create<MavenPublication>("skikoJvmRuntime") { create<MavenPublication>("skikoJvmRuntime") {
artifactId = SkikoArtifacts.runtimeArtifactIdFor(targetOs, targetArch) pomNameForPublication[name] = "Skiko JVM Runtime for ${targetOs.name} ${targetArch.name}"
artifactId = SkikoArtifacts.jvmRuntimeArtifactIdFor(targetOs, targetArch)
afterEvaluate { afterEvaluate {
artifact(skikoJvmRuntimeJar.map { it.archiveFile.get() }) artifact(skikoJvmRuntimeJar.map { it.archiveFile.get() })
var jvmSourcesArtifact: Any? = null
kotlin.jvm().mavenPublication {
jvmSourcesArtifact = artifacts.find { it.classifier == "sources" }
}
if (jvmSourcesArtifact == null) {
error("Could not find sources jar artifact for JVM target")
} else {
artifact(jvmSourcesArtifact)
}
} }
} }
if (supportWasm) { if (supportWasm) {
create<MavenPublication>("skikoWasmRuntime") { create<MavenPublication>("skikoWasmRuntime") {
pomNameForPublication[name] = "Skiko WASM Runtime"
artifactId = SkikoArtifacts.jsWasmArtifactId artifactId = SkikoArtifacts.jsWasmArtifactId
artifact(skikoWasmJar.get()) artifact(skikoWasmJar.get())
artifact(emptySourcesJar)
} }
} }
val publicationsWithoutPomNames = publications.filter { it.name !in pomNameForPublication }
if (publicationsWithoutPomNames.isNotEmpty()) {
error("Publications with unknown POM names: ${publicationsWithoutPomNames.joinToString { "'$it'" }}")
}
configureEach {
this as MavenPublication
pom.name.set(pomNameForPublication[name]!!)
}
}
}
if (skiko.isCIBuild || skiko.signArtifacts) {
signing {
sign(publishing.publications)
useInMemoryPgpKeys(skiko.signArtifactsKey, skiko.signArtifactsPassword)
} }
} }
......
...@@ -9,4 +9,6 @@ repositories { ...@@ -9,4 +9,6 @@ repositories {
dependencies { dependencies {
implementation(kotlin("stdlib")) implementation(kotlin("stdlib"))
compileOnly(gradleApi()) compileOnly(gradleApi())
api(project(":publishing"))
} }
import org.gradle.kotlin.dsl.gradleKotlinDsl
plugins {
id("org.jetbrains.kotlin.jvm")
}
repositories {
mavenCentral()
}
dependencies {
compileOnly(gradleApi())
compileOnly(gradleKotlinDsl())
compileOnly(kotlin("stdlib"))
val jacksonVersion = "2.12.5"
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:$jacksonVersion")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
implementation("io.ktor:ktor-client-okhttp:1.6.2")
implementation("org.apache.tika:tika-parsers:1.24.1")
implementation("org.jsoup:jsoup:1.14.3")
implementation("de.undercouch:gradle-download-task:4.1.2")
}
package org.jetbrains.compose.internal.gradle.publishing
import de.undercouch.gradle.tasks.download.DownloadAction
import org.gradle.api.DefaultTask
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskAction
import org.jsoup.Jsoup
import java.net.URL
abstract class DownloadFromSpaceMavenRepoTask : DefaultTask() {
@get:Internal
abstract val modulesToDownload: ListProperty<ModuleToUpload>
@get:Internal
abstract val spaceRepoUrl: Property<String>
@TaskAction
fun run() {
for (module in modulesToDownload.get()) {
downloadArtifactsFromComposeDev(module)
}
}
private fun downloadArtifactsFromComposeDev(module: ModuleToUpload) {
val groupUrl = module.groupId.replace(".", "/")
val filesListingDocument =
Jsoup.connect("${spaceRepoUrl.get()}/$groupUrl/${module.artifactId}/${module.version}/").get()
val downloadableFiles = HashMap<String, URL>()
for (a in filesListingDocument.select("#contents > a")) {
val href = a.attributes().get("href")
val lastPart = href.substringAfterLast("/", "")
// check if URL points to a file
if (lastPart.isNotEmpty() && lastPart.contains(".")) {
downloadableFiles[lastPart] = URL(href)
}
}
val destinationDir = module.localDir
if (destinationDir.exists()) {
if (module.version.endsWith("-SNAPSHOT")) {
destinationDir.deleteRecursively()
} else {
// delete existing files, that are not downloadable
val existingFiles = (destinationDir.list() ?: emptyArray()).toSet()
for (existingFileName in existingFiles) {
if (existingFileName !in downloadableFiles) {
destinationDir.resolve(existingFileName).delete()
}
}
// don't re-download all files for non-snapshot version
val it = downloadableFiles.entries.iterator()
while (it.hasNext()) {
val (fileName, _) = it.next()
if (fileName in existingFiles) {
it.remove()
}
}
}
}
DownloadAction(project, this).apply {
src(downloadableFiles.values)
dest(destinationDir)
}.execute()
}
}
\ No newline at end of file
package org.jetbrains.compose.internal.gradle.publishing
import java.io.File
data class ModuleToUpload(
val groupId: String,
val artifactId: String,
val version: String,
val localDir: File
) {
internal fun listFiles(): Array<File> =
localDir.listFiles() ?: emptyArray()
internal val coordinate: String
get() = "$groupId:$artifactId:$version"
}
\ No newline at end of file
package org.jetbrains.compose.internal.gradle.publishing
import org.gradle.api.DefaultTask
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskAction
import org.jetbrains.compose.internal.gradle.publishing.sonatype.ModuleValidator
import org.jetbrains.compose.internal.gradle.publishing.sonatype.SonatypeApi
import org.jetbrains.compose.internal.gradle.publishing.sonatype.SonatypeRestApiClient
import org.jetbrains.compose.internal.gradle.publishing.sonatype.StagingProfile
abstract class UploadToSonatypeTask : DefaultTask() {
// the task must always re-run anyway, so all inputs can be declared Internal
@get:Internal
abstract val sonatypeServer: Property<String>
@get:Internal
abstract val user: Property<String>
@get:Internal
abstract val password: Property<String>
@get:Internal
abstract val stagingProfileName: Property<String>
@get:Internal
abstract val autoCommitOnSuccess: Property<Boolean>
@get:Internal
abstract val autoDropOnError: Property<Boolean>
@get:Internal
abstract val version: Property<String>
@get:Internal
abstract val modulesToUpload: ListProperty<ModuleToUpload>
@TaskAction
fun run() {
SonatypeRestApiClient(
sonatypeServer = sonatypeServer.get(),
user = user.get(),
password = password.get(),
logger = logger
).use { client -> run(client) }
}
private fun run(sonatype: SonatypeApi) {
val stagingProfiles = sonatype.stagingProfiles()
val stagingProfileName = stagingProfileName.get()
val stagingProfile = stagingProfiles.data.firstOrNull { it.name == stagingProfileName }
?: error(
"Cannot find staging profile '$stagingProfileName' among existing staging profiles: " +
stagingProfiles.data.joinToString { "'${it.name}'" }
)
val modules = modulesToUpload.get()
validate(stagingProfile, modules)
val stagingRepo = sonatype.createStagingRepo(
stagingProfile, "Staging repo for '${stagingProfile.name}' release '${version.get()}'"
)
try {
for (module in modules) {
sonatype.upload(stagingRepo, module)
}
if (autoCommitOnSuccess.get()) {
sonatype.closeStagingRepo(stagingRepo)
}
} catch (e: Exception) {
if (autoDropOnError.get()) {
sonatype.dropStagingRepo(stagingRepo)
}
throw e
}
}
private fun validate(stagingProfile: StagingProfile, modules: List<ModuleToUpload>) {
val validationIssues = arrayListOf<Pair<ModuleToUpload, ModuleValidator.Status.Error>>()
for (module in modules) {
val status = ModuleValidator(stagingProfile, module, version.get()).validate()
if (status is ModuleValidator.Status.Error) {
validationIssues.add(module to status)
}
}
if (validationIssues.isNotEmpty()) {
val message = buildString {
appendLine("Some modules violate Maven Central requirements:")
for ((module, status) in validationIssues) {
appendLine("* ${module.coordinate} (files: ${module.localDir})")
for (error in status.errors) {
appendLine(" * $error")
}
}
}
error(message)
}
}
}
\ No newline at end of file
package org.jetbrains.compose.internal.gradle.publishing.sonatype
import okhttp3.*
import okhttp3.internal.http.RealResponseBody
import okio.Buffer
import org.gradle.api.logging.Logger
import java.net.URL
import java.time.Duration
import java.util.concurrent.atomic.AtomicLong
internal class RestApiClient(
private val serverUrl: String,
private val user: String,
private val password: String,
private val logger: Logger,
) : AutoCloseable {
private val okClient by lazy {
OkHttpClient.Builder()
.readTimeout(Duration.ofMinutes(1))
.build()
}
fun buildRequest(urlPath: String, configure: Request.Builder.() -> Unit): Request =
Request.Builder().apply {
addHeader("Authorization", Credentials.basic(user, password))
url(URL("$serverUrl/$urlPath"))
configure()
}.build()
fun <T> execute(
request: Request,
retries: Int = 5,
delaySec: Long = 10,
processResponse: (ResponseBody) -> T
): T {
val message = "Remote request #${globalRequestCounter.incrementAndGet()}"
val startTimeNs = System.nanoTime()
logger.info("$message: ${request.method} '${request.url}'")
val delayMs = delaySec * 1000
for (i in 1..retries) {
try {
return okClient.newCall(request).execute().use { response ->
val endTimeNs = System.nanoTime()
logger.info("$message: finished in ${(endTimeNs - startTimeNs)/1_000_000} ms")
if (!response.isSuccessful)
throw RequestError(request, response)
val responseBody = response.body ?: RealResponseBody(null, 0, Buffer())
processResponse(responseBody)
}
} catch (e: Exception) {
if (i == retries) {
throw RuntimeException("$message: failed all $retries attempts, see nested exception for details", e)
}
logger.info("$message: retry #$i of $retries failed. Retrying in $delayMs ms\n${e.message}")
Thread.sleep(delayMs)
}
}
error("Unreachable")
}
override fun close() {
okClient.connectionPool.evictAll()
}
companion object {
private val globalRequestCounter = AtomicLong()
}
}
package org.jetbrains.compose.internal.gradle.publishing.sonatype
import com.fasterxml.jackson.annotation.JsonRootName
import org.jetbrains.compose.internal.gradle.publishing.ModuleToUpload
import java.io.File
internal class ModuleValidator(
private val stagingProfile: StagingProfile,
private val module: ModuleToUpload,
private val version: String
) {
private val errors = arrayListOf<String>()
private var status: Status? = null
sealed class Status {
object OK : Status()
class Error(val errors: List<String>) : Status()
}
fun validate(): Status {
if (status == null) {
validateImpl()
status = if (errors.isEmpty()) Status.OK
else Status.Error(errors)
}
return status!!
}
private fun validateImpl() {
if (!module.groupId.startsWith(stagingProfile.name)) {
errors.add("Module's group id '${module.groupId}' does not match staging repo '${stagingProfile.name}'")
}
if (module.version != version) {
errors.add("Unexpected version '${module.version}' (expected: '$version')")
}
val pomFile = artifactFile(extension = "pom")
val pom = when {
pomFile.exists() ->
try {
// todo: validate POM
Xml.deserialize<Pom>(pomFile.readText())
} catch (e: Exception) {
errors.add("Cannot deserialize $pomFile: $e")
null
}
else -> null
}
val packageFile = artifactFile(extension = pom?.packaging ?: "jar")
val sourcesJar = artifactFile(extension = "jar", classifier = "sources")
val javadocJar = artifactFile(extension = "jar", classifier = "javadoc")
val nonExistingFiles = listOf(pomFile, packageFile, sourcesJar, javadocJar)
.filter { !it.exists() }
if (nonExistingFiles.isNotEmpty()) {
errors.add("Some necessary files do not exist: [${nonExistingFiles.map { it.name }.joinToString()}]")
}
// signatures and checksums should not be signed themselves
val skipSignatureCheckExtensions = setOf("asc", "md5", "sha1", "sha256", "sha512")
val unsignedFiles = module.listFiles()
.filter {
it.extension !in skipSignatureCheckExtensions && !it.resolveSibling(it.name + ".asc").exists()
}
if (unsignedFiles.isNotEmpty()) {
errors.add("Some files are not signed: [${unsignedFiles.map { it.name }.joinToString()}]")
}
}
private fun artifactFile(extension: String, classifier: String? = null): File {
val fileName = buildString {
append("${module.artifactId}-${module.version}")
if (classifier != null)
append("-$classifier")
append(".$extension")
}
return module.localDir.resolve(fileName)
}
}
@JsonRootName("project")
private data class Pom(
var groupId: String? = null,
var artifactId: String? = null,
var packaging: String? = null,
var name: String? = null,
var description: String? = null,
var url: String? = null,
var scm: Scm? = null,
var licenses: List<License>? = null,
var developers: List<Developer>? = null,
) {
internal data class Scm(
var connection: String?,
var developerConnection: String?,
var url: String?,
)
internal data class License(
var name: String? = null,
var url: String? = null
)
internal data class Developer(
var name: String? = null,
var organization: String? = null,
var organizationUrl: String? = null
)
}
package org.jetbrains.compose.internal.gradle.publishing.sonatype
import okhttp3.Request
import okhttp3.Response
internal class RequestError(
val request: Request,
val response: Response,
responseBody: String
) : RuntimeException("${request.url}: returned ${response.code}\n${responseBody.trim()}")
internal fun RequestError(request: Request, response: Response): RequestError {
var responseBodyException: Throwable? = null
val responseBody = try {
response.body?.string() ?: ""
} catch (t: Throwable) {
responseBodyException = t
""
}
return RequestError(request, response, responseBody).apply {
if (responseBodyException != null) addSuppressed(responseBodyException)
}
}
\ No newline at end of file
package org.jetbrains.compose.internal.gradle.publishing.sonatype
import com.fasterxml.jackson.annotation.JsonRootName
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper
import org.jetbrains.compose.internal.gradle.publishing.ModuleToUpload
interface SonatypeApi {
fun upload(repo: StagingRepo, module: ModuleToUpload)
fun stagingProfiles(): StagingProfiles
fun createStagingRepo(profile: StagingProfile, description: String): StagingRepo
fun dropStagingRepo(repo: StagingRepo)
fun closeStagingRepo(repo: StagingRepo)
}
@JsonRootName("stagingProfile")
data class StagingProfile(
var id: String = "",
var name: String = "",
)
@JsonRootName("stagingProfiles")
class StagingProfiles(
@JacksonXmlElementWrapper
var data: List<StagingProfile>
)
data class StagingRepo(
val id: String,
val description: String,
val profile: StagingProfile
) {
constructor(
response: PromoteResponse,
profile: StagingProfile
) : this(
id = response.data.stagedRepositoryId!!,
description = response.data.description,
profile = profile
)
@JsonRootName("promoteRequest")
data class PromoteRequest(var data: PromoteData)
@JsonRootName("promoteResponse")
data class PromoteResponse(var data: PromoteData)
data class PromoteData(var stagedRepositoryId: String? = null, var description: String)
}
package org.jetbrains.compose.internal.gradle.publishing.sonatype
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.ResponseBody
import org.apache.tika.Tika
import org.gradle.api.logging.Logger
import org.jetbrains.compose.internal.gradle.publishing.ModuleToUpload
import java.io.Closeable
import java.io.File
// https://support.sonatype.com/hc/en-us/articles/213465868-Uploading-to-a-Staging-Repository-via-REST-API
class SonatypeRestApiClient(
sonatypeServer: String,
user: String,
password: String,
private val logger: Logger,
) : SonatypeApi, Closeable {
private val client = RestApiClient(sonatypeServer, user, password, logger)
private fun buildRequest(urlPath: String, builder: Request.Builder.() -> Unit): Request =
client.buildRequest(urlPath, builder)
private fun <T> Request.execute(processResponse: (ResponseBody) -> T): T =
client.execute(this, processResponse = processResponse)
override fun close() {
client.close()
}
override fun upload(repo: StagingRepo, module: ModuleToUpload) {
for (file in module.localDir.listFiles()!!) {
uploadFile(repo, module, file)
}
}
private fun uploadFile(repo: StagingRepo, module: ModuleToUpload, file: File) {
val fileType = Tika().detect(file.name)
logger.info("Uploading $file (detected type='$fileType', length=${file.length()})")
val deployUrl = "service/local/staging/deployByRepositoryId/${repo.id}"
val groupUrl = module.groupId.replace(".", "/")
val coordinateUrl = "$groupUrl/${module.artifactId}/${module.version}"
val uploadUrlPath = "$deployUrl/$coordinateUrl/${file.name}"
buildRequest(uploadUrlPath) {
header("Content-type", fileType)
put(file.asRequestBody(fileType.toMediaTypeOrNull()))
}.execute { }
}
override fun stagingProfiles(): StagingProfiles =
buildRequest("service/local/staging/profiles") {
get()
}.execute { responseBody ->
Xml.deserialize(responseBody.string())
}
override fun createStagingRepo(profile: StagingProfile, description: String): StagingRepo {
logger.info("Creating sonatype staging repository for `${profile.id}` with description `$description`")
val response =
buildRequest("service/local/staging/profiles/${profile.id}/start") {
val promoteRequest = StagingRepo.PromoteRequest(
StagingRepo.PromoteData(description = description)
)
post(Xml.serialize(promoteRequest).toRequestBody(Xml.mediaType))
}.execute { responseBody ->
Xml.deserialize<StagingRepo.PromoteResponse>(responseBody.string())
}
return StagingRepo(response, profile)
}
override fun dropStagingRepo(repo: StagingRepo) {
stagingRepoAction("drop", repo)
}
override fun closeStagingRepo(repo: StagingRepo) {
stagingRepoAction("finish", repo)
}
private fun stagingRepoAction(
action: String, repo: StagingRepo
) {
val logRepoDescription = "profileId='${repo.profile.id}', repoId='${repo.id}', description='${repo.description}'"
logger.info("Starting '$action': $logRepoDescription")
buildRequest("service/local/staging/${repo.profile.id}/$action") {
val promoteRequest = StagingRepo.PromoteRequest(
StagingRepo.PromoteData(stagedRepositoryId = repo.id, description = repo.description)
)
post(Xml.serialize(promoteRequest).toRequestBody(Xml.mediaType))
}.execute { responseBody ->
logger.info("Finished '$action': $logRepoDescription")
logger.info("Response: '${responseBody.string()}'")
}
}
}
package org.jetbrains.compose.internal.gradle.publishing.sonatype
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule
import com.fasterxml.jackson.dataformat.xml.XmlMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import okhttp3.MediaType.Companion.toMediaType
internal object Xml {
val mediaType = "application/xml".toMediaType()
fun serialize(value: Any): String =
kotlinXmlMapper.writeValueAsString(value)
inline fun <reified T> deserialize(xml: String): T =
kotlinXmlMapper.readValue(xml, T::class.java)
private val kotlinXmlMapper: ObjectMapper =
XmlMapper(JacksonXmlModule().apply {
setDefaultUseWrapper(false)
}).registerKotlinModule()
.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
}
include(":publishing")
\ No newline at end of file
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.provider.Provider
import java.io.File import java.io.File
enum class OS( enum class OS(
...@@ -127,6 +128,17 @@ class SkikoProperties(private val myProject: Project) { ...@@ -127,6 +128,17 @@ class SkikoProperties(private val myProject: Project) {
val isRelease: Boolean val isRelease: Boolean
get() = myProject.findProperty("deploy.release") == "true" get() = myProject.findProperty("deploy.release") == "true"
val signArtifacts: Boolean
get() = myProject.findProperty("deploy.sign") == "true"
val signArtifactsKey: String
get() = System.getenv("MAVEN_ARTIFACTS_SIGN_KEY")
?: error("Environment variable 'MAVEN_ARTIFACTS_SIGN_KEY' is not specified")
val signArtifactsPassword: String
get() = System.getenv("MAVEN_ARTIFACTS_SIGN_PASSWORD")
?: error("Environment variable 'MAVEN_ARTIFACTS_SIGN_PASSWORD' is not specified")
val buildType: SkiaBuildType val buildType: SkiaBuildType
get() = if (myProject.findProperty("skiko.debug") == "true") SkiaBuildType.DEBUG else SkiaBuildType.RELEASE get() = if (myProject.findProperty("skiko.debug") == "true") SkiaBuildType.DEBUG else SkiaBuildType.RELEASE
...@@ -177,13 +189,59 @@ class SkikoProperties(private val myProject: Project) { ...@@ -177,13 +189,59 @@ class SkikoProperties(private val myProject: Project) {
val dependenciesDir: File val dependenciesDir: File
get() = myProject.rootProject.projectDir.resolve("dependencies") get() = myProject.rootProject.projectDir.resolve("dependencies")
val mavenCentral = MavenCentralProperties(myProject)
}
class MavenCentralProperties(private val myProject: Project) {
private fun propertyProvider(
property: String,
envVar: String? = null,
defaultValue: String? = null
): Provider<String> {
val providers = myProject.providers
var result = providers.gradleProperty(property)
if (envVar != null) {
result = result.orElse(providers.environmentVariable(envVar))
}
if (defaultValue != null) {
result = result.orElse(defaultValue)
} else {
result = result.orElse(providers.provider {
val envVarMessage = if (envVar != null) " or '$envVar' environment variable" else ""
error("Provide value for '$property' Gradle property$envVarMessage")
})
}
return result
}
private fun environmentVariable(variable: String) =
myProject.providers.environmentVariable(variable)
val version: Provider<String> =
propertyProvider("maven.central.version")
val user: Provider<String> =
propertyProvider("maven.central.user", envVar = "MAVEN_CENTRAL_USER")
val password: Provider<String> =
propertyProvider("maven.central.password", envVar = "MAVEN_CENTRAL_PASSWORD")
val autoCommitOnSuccess: Provider<Boolean> =
propertyProvider("maven.central.staging.auto.commit", defaultValue = "false")
.map { it.toBoolean() }
val autoDropOnError: Provider<Boolean> =
propertyProvider("maven.central.staging.auto.drop", defaultValue = "false")
.map { it.toBoolean() }
} }
object SkikoArtifacts { object SkikoArtifacts {
// names are also used in samples, e.g. samples/SkijaInjectSample/build.gradle // names are also used in samples, e.g. samples/SkijaInjectSample/build.gradle
val commonArtifactId = "skiko"
val jvmArtifactId = "skiko-jvm" val jvmArtifactId = "skiko-jvm"
val jsWasmArtifactId = "skiko-js-wasm-runtime" val jsWasmArtifactId = "skiko-js-wasm-runtime"
fun runtimeArtifactIdFor(os: OS, arch: Arch) = fun jvmRuntimeArtifactIdFor(os: OS, arch: Arch) =
"skiko-jvm-runtime-${targetId(os, arch)}" "skiko-jvm-runtime-${targetId(os, arch)}"
fun nativeRuntimeArtifactIdFor(os: OS, arch: Arch) = fun nativeRuntimeArtifactIdFor(os: OS, arch: Arch) =
"skiko-native-runtime-${targetId(os, arch)}" "skiko-native-runtime-${targetId(os, arch)}"
......
...@@ -2,6 +2,7 @@ import org.gradle.api.Task ...@@ -2,6 +2,7 @@ import org.gradle.api.Task
import org.gradle.api.provider.Provider import org.gradle.api.provider.Provider
import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.TaskContainer
import org.gradle.kotlin.dsl.register import org.gradle.kotlin.dsl.register
import java.util.*
// Utils, that are not needed in scripts can be placed to internal/utils // Utils, that are not needed in scripts can be placed to internal/utils
...@@ -15,10 +16,7 @@ inline fun <reified T : Task> TaskContainer.registerOrGetTask( ...@@ -15,10 +16,7 @@ inline fun <reified T : Task> TaskContainer.registerOrGetTask(
} }
fun joinToTitleCamelCase(vararg parts: String): String = fun joinToTitleCamelCase(vararg parts: String): String =
parts.joinToString(separator = "") { part -> parts.joinToString(separator = "") { toTitleCase(it) }
if (part.isEmpty()) part
else buildString { fun toTitleCase(s: String): String =
append(part.first().toTitleCase()) s.capitalize(Locale.ROOT)
append(part.substring(1)) \ No newline at end of file
}
}
\ No newline at end of file
import org.kohsuke.github.* import org.kohsuke.github.*
import org.jetbrains.compose.internal.gradle.publishing.*
val skiko = SkikoProperties(project) val skiko = SkikoProperties(project)
val GITHUB_REPO = "JetBrains/skiko" val GITHUB_REPO = "JetBrains/skiko"
...@@ -7,25 +8,28 @@ fun connectToGitHub() = ...@@ -7,25 +8,28 @@ fun connectToGitHub() =
.withOAuthToken(System.getenv("SKIKO_GH_RELEASE_TOKEN")) .withOAuthToken(System.getenv("SKIKO_GH_RELEASE_TOKEN"))
.build() .build()
repositories { repositories {
maven(skiko.composeRepoUrl) maven(skiko.composeRepoUrl)
} }
val skikoArtifacts by configurations.creating val skikoArtifactIds: List<String> =
listOf(
SkikoArtifacts.commonArtifactId,
SkikoArtifacts.jvmArtifactId,
SkikoArtifacts.jvmRuntimeArtifactIdFor(OS.Windows, Arch.X64),
SkikoArtifacts.jvmRuntimeArtifactIdFor(OS.Linux, Arch.X64),
SkikoArtifacts.jvmRuntimeArtifactIdFor(OS.Linux, Arch.Arm64),
SkikoArtifacts.jvmRuntimeArtifactIdFor(OS.MacOS, Arch.X64),
SkikoArtifacts.jvmRuntimeArtifactIdFor(OS.MacOS, Arch.Arm64),
)
val githubArtifacts by configurations.creating
dependencies { dependencies {
fun skikoDep(artifact: String) { for (artifactId in skikoArtifactIds) {
skikoArtifacts("org.jetbrains.skiko:$artifact:${skiko.deployVersion}") { githubArtifacts("org.jetbrains.skiko:$artifactId:${skiko.deployVersion}") {
isTransitive = false isTransitive = false
} }
} }
skikoDep(SkikoArtifacts.jvmArtifactId)
skikoDep(SkikoArtifacts.runtimeArtifactIdFor(OS.Windows, Arch.X64))
skikoDep(SkikoArtifacts.runtimeArtifactIdFor(OS.Linux, Arch.X64))
skikoDep(SkikoArtifacts.runtimeArtifactIdFor(OS.Linux, Arch.Arm64))
skikoDep(SkikoArtifacts.runtimeArtifactIdFor(OS.MacOS, Arch.X64))
skikoDep(SkikoArtifacts.runtimeArtifactIdFor(OS.MacOS, Arch.Arm64))
} }
val createGithubRelease by tasks.registering { val createGithubRelease by tasks.registering {
...@@ -40,7 +44,7 @@ val createGithubRelease by tasks.registering { ...@@ -40,7 +44,7 @@ val createGithubRelease by tasks.registering {
.commitish(githubCommit) .commitish(githubCommit)
.create() .create()
skikoArtifacts githubArtifacts
.filter { it.extension == "jar" } .filter { it.extension == "jar" }
.forEach { release.uploadAsset(it, "application/zip") } .forEach { release.uploadAsset(it, "application/zip") }
} }
...@@ -53,3 +57,38 @@ val deleteGithubRelease by tasks.registering { ...@@ -53,3 +57,38 @@ val deleteGithubRelease by tasks.registering {
repo.listReleases().firstOrNull { it.tagName == "v${skiko.releaseGithubVersion}" }?.delete() repo.listReleases().firstOrNull { it.tagName == "v${skiko.releaseGithubVersion}" }?.delete()
} }
} }
fun skikoMavenModules(version: Provider<String>): Provider<List<ModuleToUpload>> =
version.map { version ->
val artifactsDir = buildDir.resolve("skiko-artifacts")
skikoArtifactIds.map { artifactId ->
val skikoGroupId = "org.jetbrains.skiko"
ModuleToUpload(
groupId = skikoGroupId,
artifactId = artifactId,
version = version,
localDir = artifactsDir.resolve("$version/$skikoGroupId/$artifactId")
)
}
}
val downloadSkikoArtifactsFromComposeDev by tasks.registering(DownloadFromSpaceMavenRepoTask::class) {
modulesToDownload.set(skikoMavenModules(skiko.mavenCentral.version))
spaceRepoUrl.set("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
val uploadSkikoArtifactsToMavenCentral by tasks.registering(UploadToSonatypeTask::class) {
dependsOn(downloadSkikoArtifactsFromComposeDev)
val central = skiko.mavenCentral
version.set(central.version)
modulesToUpload.set(skikoMavenModules(central.version))
sonatypeServer.set("https://oss.sonatype.org")
user.set(central.user)
password.set(central.password)
autoCommitOnSuccess.set(central.autoCommitOnSuccess)
autoDropOnError.set(central.autoDropOnError)
stagingProfileName.set("org.jetbrains.skiko")
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment