Browse Source
* 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
23 changed files with 1585 additions and 0 deletions
@ -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() |
||||
} |
Binary file not shown.
@ -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 |
@ -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" "$@" |
@ -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 |
@ -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 |
||||
} |
@ -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() |
||||
} |
||||
} |
@ -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")) |
||||
} |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
} |
@ -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" |
||||
} |
@ -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) |
||||
} |
||||
} |
||||
} |
@ -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() |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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 |
||||
) |
||||
} |
@ -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") |
||||
|
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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) |
||||
} |
@ -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()}'") |
||||
} |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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) |
||||
} |
Loading…
Reference in new issue