Browse Source

Build helpers (#1350)

* Move publishing helpers from Skiko

Copy of 0c4350b8e8/skiko/buildSrc/publishing

* Fix validation of pom packaging

* Add util for reading Maven Central configuration

* Suppress unused inspection

* Rename sonatype package to utils

* Fixup copyright

* Add task for fixing modules

* Find modules in space

* Fix java-base configuration
pull/1375/head
Alexey Tsvetkov 3 years ago committed by GitHub
parent
commit
1c25803032
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 76
      build-helpers/build.gradle.kts
  2. 1
      build-helpers/gradle.properties
  3. BIN
      build-helpers/gradle/wrapper/gradle-wrapper.jar
  4. 5
      build-helpers/gradle/wrapper/gradle-wrapper.properties
  5. 185
      build-helpers/gradlew
  6. 89
      build-helpers/gradlew.bat
  7. 50
      build-helpers/publishing/build.gradle.kts
  8. 75
      build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/DownloadFromSpaceTask.kt
  9. 68
      build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/FindModulesInSpaceTask.kt
  10. 111
      build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/FixModulesBeforePublishingTask.kt
  11. 59
      build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/MavenCentralProperties.kt
  12. 21
      build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/ModuleToUpload.kt
  13. 103
      build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/UploadToSonatypeTask.kt
  14. 77
      build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/AbstractRestApiClient.kt
  15. 80
      build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/Checksum.kt
  16. 120
      build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/ModuleValidator.kt
  17. 157
      build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/PomDocument.kt
  18. 28
      build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/RequestError.kt
  19. 51
      build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/SonatypeApi.kt
  20. 102
      build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/SonatypeRestApiClient.kt
  21. 95
      build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/SpaceApiClient.kt
  22. 31
      build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/Xml.kt
  23. 1
      build-helpers/settings.gradle.kts

76
build-helpers/build.gradle.kts

@ -0,0 +1,76 @@
plugins {
kotlin("jvm") version "1.5.30" apply false
id("com.github.johnrengelman.shadow") version "7.1.0" apply false
}
subprojects {
group = "org.jetbrains.compose.internal.build-helpers"
version = project.property("deploy.version") as String
repositories {
mavenCentral()
}
plugins.withType(JavaBasePlugin::class.java) {
afterEvaluate {
configureIfExists<JavaPluginExtension> {
if (sourceSets.names.contains(SourceSet.MAIN_SOURCE_SET_NAME)) {
withJavadocJar()
withSourcesJar()
}
}
}
}
plugins.withId("maven-publish") {
configureIfExists<PublishingExtension> {
configurePublishing(project)
}
}
}
fun PublishingExtension.configurePublishing(project: Project) {
repositories {
configureEach {
val repoName = name
project.tasks.register("publishTo${repoName}") {
group = "publishing"
dependsOn(project.tasks.named("publishAllPublicationsTo${repoName}Repository"))
}
}
maven {
name = "BuildRepo"
url = uri("${rootProject.buildDir}/repo")
}
maven {
name = "ComposeInternalRepo"
url = uri(
System.getenv("COMPOSE_INTERNAL_REPO_URL")
?: "https://maven.pkg.jetbrains.space/public/p/compose/internal"
)
credentials {
username =
System.getenv("COMPOSE_INTERNAL_REPO_USERNAME")
?: System.getenv("COMPOSE_REPO_KEY")
?: ""
password =
System.getenv("COMPOSE_INTERNAL_REPO_KEY")
?: System.getenv("COMPOSE_REPO_KEY")
?: ""
}
}
}
publications {
create<MavenPublication>("main") {
groupId = project.group.toString()
artifactId = project.name
version = project.version.toString()
from(project.components["java"])
}
}
}
inline fun <reified T> Project.configureIfExists(fn: T.() -> Unit) {
extensions.findByType(T::class.java)?.fn()
}

1
build-helpers/gradle.properties

@ -0,0 +1 @@
deploy.version=0.1.0-SNAPSHOT

BIN
build-helpers/gradle/wrapper/gradle-wrapper.jar vendored

Binary file not shown.

5
build-helpers/gradle/wrapper/gradle-wrapper.properties vendored

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
build-helpers/gradlew vendored

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 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
#
# https://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.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MSYS* | MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
build-helpers/gradlew.bat vendored

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

50
build-helpers/publishing/build.gradle.kts

@ -0,0 +1,50 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.gradle.kotlin.dsl.gradleKotlinDsl
plugins {
`java`
`maven-publish`
`java-gradle-plugin`
id("org.jetbrains.kotlin.jvm")
id("com.github.johnrengelman.shadow")
}
repositories {
maven("https://maven.pkg.jetbrains.space/public/p/space/maven")
}
val embeddedDependencies by configurations.creating { isTransitive = false }
dependencies {
compileOnly(gradleApi())
compileOnly(gradleKotlinDsl())
compileOnly(kotlin("stdlib"))
fun embedded(dep: String) {
compileOnly(dep)
embeddedDependencies(dep)
}
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.4")
implementation("org.apache.tika:tika-parsers:1.24.1")
implementation("org.jsoup:jsoup:1.14.3")
implementation("org.jetbrains:space-sdk-jvm:83821-beta")
embedded("de.undercouch:gradle-download-task:4.1.2")
}
val shadow = tasks.named<ShadowJar>("shadowJar") {
val fromPackage = "de.undercouch"
val toPackage = "org.jetbrains.compose.internal.publishing.$fromPackage"
relocate(fromPackage, toPackage)
archiveClassifier.set("shadow")
configurations = listOf(embeddedDependencies)
exclude("META-INF/gradle-plugins/de.undercouch.download.properties")
}
val jar = tasks.named<Jar>("jar") {
dependsOn(shadow)
from(zipTree(shadow.get().archiveFile))
this.duplicatesStrategy = DuplicatesStrategy.INCLUDE
}

75
build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/DownloadFromSpaceTask.kt

@ -0,0 +1,75 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.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
@Suppress("unused") // public api
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()
}
}

68
build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/FindModulesInSpaceTask.kt

@ -0,0 +1,68 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.publishing
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.jetbrains.compose.internal.publishing.utils.SpaceApiClient
import space.jetbrains.api.runtime.types.PackageRepositoryIdentifier
import space.jetbrains.api.runtime.types.ProjectIdentifier
abstract class FindModulesInSpaceTask : DefaultTask() {
@get:Input
abstract val requestedGroupId: Property<String>
@get:Input
abstract val requestedVersion: Property<String>
@get:Input
abstract val spaceInstanceUrl: Property<String>
@get:Internal
abstract val spaceClientId: Property<String>
@get:Internal
abstract val spaceClientSecret: Property<String>
@get:Input
abstract val spaceProjectId: Property<String>
@get:Input
abstract val spaceRepoId: Property<String>
@get:OutputFile
abstract val modulesTxtFile: RegularFileProperty
@TaskAction
fun run() {
val space = SpaceApiClient(
serverUrl = spaceInstanceUrl.get(),
clientId = spaceClientId.get(),
clientSecret = spaceClientSecret.get()
)
val projectId = ProjectIdentifier.Id(spaceProjectId.get())
val repoId = PackageRepositoryIdentifier.Id(spaceRepoId.get())
val modules = ArrayList<String>()
val requestedGroupId = requestedGroupId.get()
val requestedVersion = requestedVersion.get()
space.forEachPackageWithVersion(projectId, repoId, requestedVersion) { pkg ->
if (pkg.groupId.startsWith(requestedGroupId)) {
modules.add("${pkg.groupId}:${pkg.artifactId}:${pkg.version}")
}
}
modulesTxtFile.get().asFile.apply {
parentFile.mkdirs()
writeText(modules.joinToString("\n"))
}
}
}

111
build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/FixModulesBeforePublishingTask.kt

@ -0,0 +1,111 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.publishing
import org.jetbrains.compose.internal.publishing.utils.*
import org.gradle.api.*
import org.gradle.api.tasks.*
import org.gradle.api.file.*
import org.gradle.plugins.signing.SigningExtension
import org.gradle.plugins.signing.signatory.Signatory
import org.gradle.plugins.signing.type.pgp.ArmoredSignatureType
import java.io.File
import java.util.jar.JarOutputStream
@Suppress("unused") // public api
abstract class FixModulesBeforePublishingTask : DefaultTask() {
@get:InputFiles
abstract val inputRepoDir: DirectoryProperty
@get:OutputDirectory
abstract val outputRepoDir: DirectoryProperty
@get:Nested
val signatory: Signatory
get() = project.extensions.getByType(SigningExtension::class.java).signatory
private val checksums: Checksum = defaultChecksums()
@TaskAction
fun run() {
val inputDir = inputRepoDir.get().asFile
val outputDir = outputRepoDir.get().asFile.apply {
deleteRecursively()
mkdirs()
}
for (inputFile in inputDir.walk()) {
if (inputFile.isDirectory
|| checksums.isChecksumFile(inputFile)
|| inputFile.name.endsWith(".asc")
) continue
val outputFile = outputDir.resolve(inputFile.relativeTo(inputDir).path)
outputFile.parentFile.mkdirs()
logger.info("Copying and processing $inputFile to $outputFile")
if (inputFile.name.endsWith(".pom", ignoreCase = true)) {
val pom = PomDocument(inputFile)
fixPomIfNeeded(pom)
pom.saveTo(outputFile)
if (pom.packaging != "pom") {
fixSourcesAndJavadocJarIfNeeded(
inputDir = inputFile.parentFile,
outputDir = outputFile.parentFile,
baseName = inputFile.nameWithoutExtension
)
}
} else {
inputFile.copyTo(outputFile)
}
}
for (outputFile in outputDir.walk().filter { it.isFile }) {
// todo: make parallel
val signatureFile = outputFile.generateSignature()
checksums.generateChecksumFilesFor(outputFile)
checksums.generateChecksumFilesFor(signatureFile)
}
}
private fun fixPomIfNeeded(pom: PomDocument) {
pom.fillMissingTags(
projectUrl = "https://github.com/JetBrains/compose-jb",
projectInceptionYear = "2020",
licenseName = "The Apache Software License, Version 2.0",
licenseUrl = "https://www.apache.org/licenses/LICENSE-2.0.txt",
licenseDistribution = "repo",
scmConnection = "scm:git:https://github.com/JetBrains/compose-jb.git",
scmDeveloperConnection = "scm:git:https://github.com/JetBrains/compose-jb.git",
scmUrl = "https://github.com/JetBrains/compose-jb",
developerName = "Compose Multiplatform Team",
developerOrganization = "JetBrains",
developerOrganizationUrl = "https://www.jetbrains.com",
)
}
private fun fixSourcesAndJavadocJarIfNeeded(inputDir: File, outputDir: File, baseName: String) {
val srcJar = inputDir.resolve("$baseName-sources.jar")
if (!srcJar.exists()) {
logger.warn("$srcJar does not exist. Generating empty stub")
outputDir.resolve(srcJar.name).generateEmptyJar()
}
val javadocJar = inputDir.resolve("$baseName-javadoc.jar")
if (!javadocJar.exists()) {
logger.warn("$javadocJar does not exist. Generating empty stub")
outputDir.resolve(javadocJar.name).generateEmptyJar()
}
}
private fun File.generateEmptyJar(): File =
apply {
JarOutputStream(this.outputStream().buffered()).use { }
}
private fun File.generateSignature(): File {
return ArmoredSignatureType().sign(signatory, this)
}
}

59
build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/MavenCentralProperties.kt

@ -0,0 +1,59 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.publishing
import org.gradle.api.Project
import org.gradle.api.provider.Provider
@Suppress("unused") // public api
class MavenCentralProperties(private val myProject: Project) {
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.close.after.upload", defaultValue = "false")
.map { it.toBoolean() }
val autoDropOnError: Provider<Boolean> =
propertyProvider("maven.central.staging.from.after.error", defaultValue = "false")
.map { it.toBoolean() }
val signArtifacts: Boolean
get() = myProject.findProperty("maven.central.sign") == "true"
val signArtifactsKey: Provider<String> =
propertyProvider("maven.central.sign.key", envVar = "MAVEN_CENTRAL_SIGN_KEY")
val signArtifactsPassword: Provider<String> =
propertyProvider("maven.central.sign.password", envVar = "MAVEN_CENTRAL_SIGN_PASSWORD")
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))
}
result = if (defaultValue != null) {
result.orElse(defaultValue)
} else {
result.orElse(providers.provider {
val envVarMessage = if (envVar != null) " or '$envVar' environment variable" else ""
error("Provide value for '$property' Gradle property$envVarMessage")
})
}
return result
}
}

21
build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/ModuleToUpload.kt

@ -0,0 +1,21 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.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"
}

103
build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/UploadToSonatypeTask.kt

@ -0,0 +1,103 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.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.publishing.utils.*
@Suppress("unused") // public api
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)
}
}
}

77
build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/AbstractRestApiClient.kt

@ -0,0 +1,77 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.publishing.utils
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()
}
}

80
build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/Checksum.kt

@ -0,0 +1,80 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.publishing.utils
import java.io.File
import java.security.MessageDigest
internal fun defaultChecksums(): Checksum = CompositeChecksum(
BasicChecksum("MD5", ".md5"),
BasicChecksum("SHA-1", ".sha1"),
BasicChecksum("SHA-256", ".sha256"),
BasicChecksum("SHA-512", ".sha512"),
)
internal abstract class Checksum {
abstract fun update(input: ByteArray)
abstract fun reset()
abstract fun write(basePath: String)
abstract fun isChecksumFile(file: File): Boolean
fun generateChecksumFilesFor(file: File) {
reset()
update(file.readBytes())
write(basePath = file.path)
}
}
private class CompositeChecksum(private vararg val checksums: Checksum) : Checksum() {
override fun update(input: ByteArray) {
checksums.forEach { it.update(input) }
}
override fun reset() {
checksums.forEach { it.reset() }
}
override fun write(basePath: String) {
checksums.forEach { it.write(basePath) }
}
override fun isChecksumFile(file: File): Boolean =
checksums.any { it.isChecksumFile(file) }
}
private class BasicChecksum(
private val md: MessageDigest,
private val checksumExt: String
) : Checksum() {
constructor(algorithm: String, extension: String) : this(MessageDigest.getInstance(algorithm), extension)
override fun update(input: ByteArray) {
md.update(input)
}
override fun reset() {
md.reset()
}
override fun write(basePath: String) {
File(basePath + checksumExt).writeHexString(md.digest())
}
override fun isChecksumFile(file: File): Boolean =
file.name.endsWith(checksumExt, ignoreCase = true)
private fun File.writeHexString(bytes: ByteArray) {
bufferedWriter().use { writer ->
for (b in bytes) {
val hex = Integer.toHexString(0xFF and b.toInt())
if (hex.length == 1) {
writer.append('0')
}
writer.append(hex)
}
}
}
}

120
build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/ModuleValidator.kt

@ -0,0 +1,120 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.publishing.utils
import com.fasterxml.jackson.annotation.JsonRootName
import org.jetbrains.compose.internal.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 mandatoryFiles = arrayListOf(pomFile)
if (pom != null && pom.packaging != "pom") {
mandatoryFiles.add(artifactFile(extension = pom.packaging ?: "jar"))
mandatoryFiles.add(artifactFile(extension = "jar", classifier = "sources"))
mandatoryFiles.add(artifactFile(extension = "jar", classifier = "javadoc"))
}
val nonExistingFiles = mandatoryFiles.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
)
}

157
build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/PomDocument.kt

@ -0,0 +1,157 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.publishing.utils
import org.w3c.dom.Document
import org.w3c.dom.Element
import org.w3c.dom.Node
import java.io.File
import java.io.StringWriter
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
/**
projectUrl = "https://github.com/JetBrains/compose-jb",
projectInceptionYear = "2020",
licenseName = "The Apache Software License, Version 2.0",
licenseUrl = "https://www.apache.org/licenses/LICENSE-2.0.txt",
licenseDistribution = "repo",
scmConnection = "scm:git:https://github.com/JetBrains/compose-jb.git",
scmDeveloperConnection = "scm:git:https://github.com/JetBrains/compose-jb.git",
scmUrl = "https://github.com/JetBrains/compose-jb",
developerName = "Compose Multiplatform Team",
developerOrganization = "JetBrains",
developerOrganizationUrl = "https://www.jetbrains.com",
*/
internal class PomDocument(file: File) {
private val doc: Document
val groupId: String?
val artifactId: String?
val version: String?
val packaging: String?
init {
doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file)
val projectNodes = doc.project.children().asMap()
groupId = projectNodes["groupId"]?.textContent
artifactId = projectNodes["artifactId"]?.textContent
version = projectNodes["version"]?.textContent
packaging = projectNodes["packaging"]?.textContent ?: "jar"
}
fun coordiateAsString() = "$groupId:$artifactId:$version"
fun saveTo(outputFile: File) {
val sw = StringWriter()
val transformer = TransformerFactory.newInstance().newTransformer().apply {
setOutputProperty(OutputKeys.ENCODING, "UTF-8")
setOutputProperty(OutputKeys.INDENT, "yes")
}
transformer.transform(DOMSource(doc), StreamResult(sw))
outputFile.bufferedWriter().use { writer ->
for (line in sw.toString().lineSequence()) {
if (line.isNotBlank()) {
writer.appendLine(line)
}
}
}
}
fun fillMissingTags(
projectUrl: String,
projectInceptionYear: String,
licenseName: String,
licenseUrl: String,
licenseDistribution: String,
scmConnection: String,
scmDeveloperConnection: String,
scmUrl: String,
developerName: String,
developerOrganization: String,
developerOrganizationUrl: String,
): Unit = with (doc) {
val originalNodes = project.children().asMap()
val nameText = originalNodes["name"]?.textContent
?: originalNodes["artifactId"]!!.textContent
.split("-")
.joinToString(" ") { it.capitalize() }
val name = newNode("name", nameText)
val description = newNode("description", (originalNodes["description"] ?: name).textContent)
val url = newNode("url", projectUrl)
val inceptionYear = newNode("inceptionYear", projectInceptionYear)
val licences =
newNode("licenses").withChildren(
newNode("license").withChildren(
newNode("name", licenseName),
newNode("url", licenseUrl),
newNode("distribution", licenseDistribution)
)
)
val scm =
newNode("scm").withChildren(
newNode("connection", scmConnection),
newNode("developerConnection", scmDeveloperConnection),
newNode("url", scmUrl)
)
val developers =
newNode("developers").withChildren(
newNode("developer").withChildren(
newNode("name", developerName),
newNode("organization", developerOrganization),
newNode("organizationUrl", developerOrganizationUrl),
)
)
val dependencies = originalNodes["dependencies"]
val nodesToInsert = listOf(
name, description, url, inceptionYear, licences, scm, developers, dependencies
).filterNotNull()
for (nodeToInsert in nodesToInsert) {
val originalNode = originalNodes[nodeToInsert.nodeName]
if (originalNode != null) {
project.removeChild(originalNode)
}
project.appendChild(nodeToInsert)
}
}
private fun Document.newNode(tag: String, value: String? = null, fn: Element.() -> Unit = {}) =
createElement(tag).apply {
if (value != null) {
appendChild(createTextNode(value))
}
fn()
}
private fun Element.withChildren(vararg nodes: Node): Element {
nodes.forEach { appendChild(it) }
return this
}
private fun Node.children(): List<Node> {
val result = ArrayList<Node>(childNodes.length)
for (i in 0 until childNodes.length) {
result.add(childNodes.item(i))
}
return result
}
private fun List<Node>.asMap(): Map<String, Node> =
associateBy { it.nodeName }
private fun Node.getChildByTag(tag: String): Node =
findChildByTag(tag) ?: error("Could not find <$tag>")
private fun Node.findChildByTag(tag: String): Node? =
children().firstOrNull { it.nodeName == tag }
private val Document.project: Node
get() = getChildByTag("project")
}

28
build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/RequestError.kt

@ -0,0 +1,28 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.publishing.utils
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)
}
}

51
build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/SonatypeApi.kt

@ -0,0 +1,51 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.publishing.utils
import com.fasterxml.jackson.annotation.JsonRootName
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper
import org.jetbrains.compose.internal.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)
}

102
build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/SonatypeRestApiClient.kt

@ -0,0 +1,102 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.publishing.utils
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.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()}'")
}
}
}

95
build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/SpaceApiClient.kt

@ -0,0 +1,95 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.publishing.utils
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import kotlinx.coroutines.runBlocking
import space.jetbrains.api.runtime.*
import space.jetbrains.api.runtime.resources.projects
import space.jetbrains.api.runtime.types.*
internal class SpaceApiClient(
private val serverUrl: String,
private val clientId: String,
private val clientSecret: String,
) {
data class PackageInfo(
val groupId: String,
val artifactId: String,
val version: String
)
fun forEachPackageWithVersion(
projectId: ProjectIdentifier,
repoId: PackageRepositoryIdentifier,
version: String,
fn: (PackageInfo) -> Unit
) {
withSpaceClient {
forEachPackage(projectId, repoId) { pkg ->
val details = projects.packages.repositories.packages.versions
.getPackageVersionDetails(
projectId, repoId, pkg.name, version
)
if (details != null) {
val split = pkg.name.split("/")
if (split.size != 2) {
error("Invalid maven package name: '${pkg.name}'")
}
fn(PackageInfo(groupId = split[0], artifactId = split[1], version = version))
}
}
}
}
private fun withSpaceClient(fn: suspend SpaceHttpClientWithCallContext.() -> Unit) {
runBlocking {
HttpClient(OkHttp).use { client ->
val space = SpaceHttpClient(client).withServiceAccountTokenSource(
serverUrl = serverUrl,
clientId = clientId,
clientSecret = clientSecret
)
space.fn()
}
}
}
private fun batches(batchSize: Int = 100) =
generateSequence(0) { it + batchSize }
.map { BatchInfo(it.toString(), batchSize) }
private suspend fun <T> forAllInAllBatches(
getBatch: suspend (BatchInfo) -> Batch<T>,
fn: suspend (T) -> Unit
) {
for (batchInfo in batches()) {
val batch = getBatch(batchInfo)
for (element in batch.data) {
fn(element)
}
if (batch.data.isEmpty() || (batch.next.toIntOrNull() ?: 0) >= (batch.totalCount ?: 0)) return
}
}
private suspend fun SpaceHttpClientWithCallContext.forEachPackage(
projectId: ProjectIdentifier,
repoId: PackageRepositoryIdentifier,
fn: suspend (PackageData) -> Unit
) {
forAllInAllBatches({ batch ->
projects.packages.repositories.packages.getAllPackages(
project = projectId,
repository = repoId,
query = "",
batchInfo = batch
)
}, fn)
}
}

31
build-helpers/publishing/src/main/kotlin/org/jetbrains/compose/internal/publishing/utils/Xml.kt

@ -0,0 +1,31 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.internal.publishing.utils
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)
}

1
build-helpers/settings.gradle.kts

@ -0,0 +1 @@
include(":publishing")
Loading…
Cancel
Save