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