From 3cf2a74cd37ca4e37035377efea551eb18217c6d Mon Sep 17 00:00:00 2001 From: stslex Date: Sun, 16 Feb 2025 16:50:28 +0300 Subject: [PATCH 1/2] add compile time loggers, optional logging for runtime --- app/build.gradle.kts | 4 ++ .../com/stslex/compiler_app/MainActivity.kt | 4 +- compiler-plugin/build.gradle.kts | 8 +--- .../compiler_plugin/DistinctChangeCache.kt | 18 +++++--- .../compiler_plugin/DistinctChangeConfig.kt | 5 ++ .../compiler_plugin/DistinctUntilChangeFun.kt | 11 ++++- .../compiler_plugin/WorkshopCompilerPlugin.kt | 2 +- .../DistinctChangeIrGenerationExtension.kt | 15 ++++-- .../transformers/IrFunctionTransformer.kt | 25 +++++----- .../compiler_plugin/utils/CompileLogger.kt | 20 ++++++++ .../compiler_plugin/utils/CompilerConsts.kt | 6 --- .../utils/CompilerExtensions.kt | 27 +++++++++-- .../utils/ReadQualifierUtil.kt | 46 +++++++++++++++++++ .../compiler_plugin/utils/RuntimeLogger.kt | 25 ++++++++++ gradle/libs.versions.toml | 2 +- settings.gradle.kts | 7 ++- 16 files changed, 180 insertions(+), 45 deletions(-) create mode 100644 compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctChangeConfig.kt create mode 100644 compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompileLogger.kt delete mode 100644 compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompilerConsts.kt create mode 100644 compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/ReadQualifierUtil.kt create mode 100644 compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/RuntimeLogger.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 61c0adf..1c8ea40 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -28,6 +28,10 @@ android { } dependencies { +// todo: for debug +// implementation(project(":compiler-plugin")) +// kotlinCompilerPluginClasspath(project(":compiler-plugin")) + implementation(libs.stslex.compilerPlugin) kotlinCompilerPluginClasspath(libs.stslex.compilerPlugin) diff --git a/app/src/main/kotlin/com/stslex/compiler_app/MainActivity.kt b/app/src/main/kotlin/com/stslex/compiler_app/MainActivity.kt index a1652fa..7082a45 100644 --- a/app/src/main/kotlin/com/stslex/compiler_app/MainActivity.kt +++ b/app/src/main/kotlin/com/stslex/compiler_app/MainActivity.kt @@ -53,13 +53,13 @@ class MainActivity : ComponentActivity() { setSecondName(user.secondName) } - @DistinctUntilChangeFun + @DistinctUntilChangeFun(false) private fun setName(name: String) { logger.log(Level.INFO, "setName: $name") findViewById(R.id.usernameFieldTextView).text = name } - @DistinctUntilChangeFun + @DistinctUntilChangeFun(true) private fun setSecondName(name: String) { logger.log(Level.INFO, "setSecondName: $name") findViewById(R.id.secondNameFieldTextView).text = name diff --git a/compiler-plugin/build.gradle.kts b/compiler-plugin/build.gradle.kts index c7fee26..30224c0 100644 --- a/compiler-plugin/build.gradle.kts +++ b/compiler-plugin/build.gradle.kts @@ -8,7 +8,7 @@ plugins { } group = "io.github.stslex" -version = "0.0.1" +version = libs.versions.stslexCompilerPlugin.get() java { sourceCompatibility = JavaVersion.VERSION_21 @@ -43,7 +43,7 @@ publishing { groupId = "io.github.stslex" artifactId = "compiler-plugin" - version = "0.0.1" + version = libs.versions.stslexCompilerPlugin.get() artifact(tasks["javadocJar"]) artifact(tasks["sourcesJar"]) @@ -94,7 +94,6 @@ tasks.named("publishToMavenLocal") { val generateChecksums by tasks.register("generateChecksums") { mustRunAfter("publishToMavenLocal") - finalizedBy("packageArtifacts") group = "publishing" description = "Generate MD5 и SHA1 for all artifacts in local repository" @@ -127,9 +126,6 @@ val generateChecksums by tasks.register("generateChecksums") { } tasks.register("packageArtifacts") { - mustRunAfter("generateChecksums") - dependsOn("generateChecksums") - group = "publishing" description = "Create ZIP-archive with artifacts for Central Publisher Portal" diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctChangeCache.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctChangeCache.kt index 73ac427..2a6b059 100644 --- a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctChangeCache.kt +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctChangeCache.kt @@ -1,19 +1,25 @@ package io.github.stslex.compiler_plugin -import java.util.logging.Level -import java.util.logging.Logger +import io.github.stslex.compiler_plugin.utils.RuntimeLogger internal object DistinctChangeCache { - private const val TAG = "KotlinCompilerDistinct" - private val logger = Logger.getLogger(TAG) private val cache = mutableMapOf, Any?>>() + private val logger = RuntimeLogger.tag("DistinctChangeLogger") @JvmStatic @Suppress("UNCHECKED_CAST") - fun invoke(key: String, args: List, body: () -> R): R { + fun invoke( + key: String, + args: List, + body: () -> R, + config: DistinctChangeConfig + ): R { val entry = cache[key] - logger.log(Level.INFO, "memorize key: $key, entry: $entry, args: $args") + + if (config.logging) { + logger.i("key: $key, config:$config, entry: $entry, args: $args") + } if (entry != null && entry.first == args) { return entry.second as R diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctChangeConfig.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctChangeConfig.kt new file mode 100644 index 0000000..49f0917 --- /dev/null +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctChangeConfig.kt @@ -0,0 +1,5 @@ +package io.github.stslex.compiler_plugin + +internal data class DistinctChangeConfig( + val logging: Boolean +) \ No newline at end of file diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctUntilChangeFun.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctUntilChangeFun.kt index 002ee18..f19aa8d 100644 --- a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctUntilChangeFun.kt +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/DistinctUntilChangeFun.kt @@ -2,4 +2,13 @@ package io.github.stslex.compiler_plugin @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.BINARY) -public annotation class DistinctUntilChangeFun \ No newline at end of file +public annotation class DistinctUntilChangeFun( + val logging: Boolean = LOGGING_DEFAULT +) { + + public companion object { + + internal const val LOGGING_DEFAULT = false + + } +} \ No newline at end of file diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/WorkshopCompilerPlugin.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/WorkshopCompilerPlugin.kt index eb5b27d..4c11a43 100644 --- a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/WorkshopCompilerPlugin.kt +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/WorkshopCompilerPlugin.kt @@ -12,6 +12,6 @@ internal class WorkshopCompilerPlugin( ) : CompilerPluginRegistrar() { override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { - IrGenerationExtension.registerExtension(DistinctChangeIrGenerationExtension()) + IrGenerationExtension.registerExtension(DistinctChangeIrGenerationExtension(configuration)) } } \ No newline at end of file diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/extensions/DistinctChangeIrGenerationExtension.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/extensions/DistinctChangeIrGenerationExtension.kt index c2bcc7a..dd6af84 100644 --- a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/extensions/DistinctChangeIrGenerationExtension.kt +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/extensions/DistinctChangeIrGenerationExtension.kt @@ -3,15 +3,24 @@ package io.github.stslex.compiler_plugin.extensions import io.github.stslex.compiler_plugin.transformers.IrFunctionTransformer import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.config.CommonConfigurationKeys +import org.jetbrains.kotlin.config.CompilerConfiguration import org.jetbrains.kotlin.ir.declarations.IrModuleFragment import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid -internal class DistinctChangeIrGenerationExtension : IrGenerationExtension { +internal class DistinctChangeIrGenerationExtension( + private val configuration: CompilerConfiguration +) : IrGenerationExtension { override fun generate( moduleFragment: IrModuleFragment, - pluginContext: IrPluginContext + pluginContext: IrPluginContext, ) { - moduleFragment.transformChildrenVoid(IrFunctionTransformer(pluginContext)) + val messageCollector = configuration.get( + CommonConfigurationKeys.MESSAGE_COLLECTOR_KEY, + MessageCollector.NONE + ) + moduleFragment.transformChildrenVoid(IrFunctionTransformer(pluginContext, messageCollector)) } } diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/transformers/IrFunctionTransformer.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/transformers/IrFunctionTransformer.kt index a1b8eaf..b761eb8 100644 --- a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/transformers/IrFunctionTransformer.kt +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/transformers/IrFunctionTransformer.kt @@ -1,35 +1,33 @@ package io.github.stslex.compiler_plugin.transformers import buildArgsListExpression -import io.github.stslex.compiler_plugin.DistinctUntilChangeFun +import io.github.stslex.compiler_plugin.utils.CompileLogger.Companion.toCompilerLogger import io.github.stslex.compiler_plugin.utils.buildLambdaForBody import io.github.stslex.compiler_plugin.utils.buildSaveInCacheCall import io.github.stslex.compiler_plugin.utils.fullyQualifiedName +import io.github.stslex.compiler_plugin.utils.readQualifier import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.ir.IrStatement import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction import org.jetbrains.kotlin.ir.declarations.createExpressionBody import org.jetbrains.kotlin.ir.expressions.impl.IrConstImpl -import org.jetbrains.kotlin.ir.util.hasAnnotation -import org.jetbrains.kotlin.name.FqName internal class IrFunctionTransformer( - private val pluginContext: IrPluginContext + private val pluginContext: IrPluginContext, + private val messageCollector: MessageCollector ) : IrElementTransformerVoidWithContext() { - private val IrSimpleFunction.isAnnotationValid: Boolean - get() = DistinctUntilChangeFun::class.qualifiedName - ?.let { qualifier -> hasAnnotation(FqName(qualifier)) } - ?: false + private val logger by lazy { messageCollector.toCompilerLogger() } override fun visitSimpleFunction(declaration: IrSimpleFunction): IrStatement { - if (declaration.isAnnotationValid.not()) { - return super.visitSimpleFunction(declaration) - } - val originalBody = declaration.body ?: return super.visitSimpleFunction(declaration) + val qualifierArgs = pluginContext.readQualifier(declaration, logger) + ?: return super.visitSimpleFunction(declaration) + val originalBody = declaration.body ?: return super.visitSimpleFunction(declaration) + logger.i("fullyQualifiedName: ${declaration.fullyQualifiedName}") val keyLiteral = IrConstImpl.string( startOffset = declaration.startOffset, endOffset = declaration.endOffset, @@ -44,11 +42,14 @@ internal class IrFunctionTransformer( argsListExpr = argsListExpr, lambdaExpr = lambdaExpr, function = declaration, + qualifierArgs = qualifierArgs, + logger = logger ) declaration.body = pluginContext.irFactory.createExpressionBody(memoizeCall) return super.visitSimpleFunction(declaration) } + } diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompileLogger.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompileLogger.kt new file mode 100644 index 0000000..ab880de --- /dev/null +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompileLogger.kt @@ -0,0 +1,20 @@ +package io.github.stslex.compiler_plugin.utils + +import org.jetbrains.kotlin.backend.common.toLogger +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity +import org.jetbrains.kotlin.cli.common.messages.MessageCollector + +internal class CompileLogger( + private val messageCollector: MessageCollector +) { + + fun i(msg: String) { + messageCollector.toLogger() + messageCollector.report(CompilerMessageSeverity.INFO, msg) + } + + companion object { + + fun MessageCollector.toCompilerLogger(): CompileLogger = CompileLogger(this) + } +} \ No newline at end of file diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompilerConsts.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompilerConsts.kt deleted file mode 100644 index 24334fe..0000000 --- a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompilerConsts.kt +++ /dev/null @@ -1,6 +0,0 @@ -package io.github.stslex.compiler_plugin.utils - -internal object CompilerConsts { - - const val PATH: String = "io.github.stslex.compiler_plugin" -} \ No newline at end of file diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompilerExtensions.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompilerExtensions.kt index 80f9e19..52263ea 100644 --- a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompilerExtensions.kt +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/CompilerExtensions.kt @@ -2,9 +2,11 @@ package io.github.stslex.compiler_plugin.utils import io.github.stslex.compiler_plugin.DistinctChangeCache import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder import org.jetbrains.kotlin.descriptors.Modality import org.jetbrains.kotlin.ir.builders.declarations.buildFun import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrDeclaration import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin import org.jetbrains.kotlin.ir.declarations.IrFunction import org.jetbrains.kotlin.ir.declarations.IrPackageFragment @@ -16,13 +18,26 @@ import org.jetbrains.kotlin.ir.expressions.impl.IrCallImpl import org.jetbrains.kotlin.ir.expressions.impl.IrFunctionExpressionImpl import org.jetbrains.kotlin.ir.types.typeWith import org.jetbrains.kotlin.ir.util.deepCopyWithSymbols +import org.jetbrains.kotlin.ir.util.dump import org.jetbrains.kotlin.ir.util.kotlinFqName import org.jetbrains.kotlin.ir.util.patchDeclarationParents import org.jetbrains.kotlin.name.CallableId import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name +import kotlin.reflect.KClass +internal fun IrPluginContext.createIrBuilder( + declaration: IrDeclaration +): DeclarationIrBuilder = DeclarationIrBuilder( + generatorContext = this, + symbol = declaration.symbol +) + +internal inline fun KClass.toClassId(): ClassId = ClassId( + FqName(java.`package`.name), + Name.identifier(java.simpleName) +) internal fun IrPluginContext.buildLambdaForBody( originalBody: IrBody, @@ -78,13 +93,14 @@ internal fun IrPluginContext.buildSaveInCacheCall( argsListExpr: IrExpression, lambdaExpr: IrExpression, function: IrSimpleFunction, + qualifierArgs: IrExpression, + logger: CompileLogger ): IrExpression { - val cacheNameClass = checkNotNull(DistinctChangeCache::class.simpleName) { - "Cannot get simpleName" - } + logger.i("buildSaveInCacheCall for ${function.name}, args: ${argsListExpr.dump()} with config: ${qualifierArgs.dump()}") + val memoizeFunction = referenceFunctions( CallableId( - classId = ClassId(FqName(CompilerConsts.PATH), Name.identifier(cacheNameClass)), + classId = DistinctChangeCache::class.toClassId(), callableName = Name.identifier("invoke") ) ) @@ -97,7 +113,7 @@ internal fun IrPluginContext.buildSaveInCacheCall( type = function.returnType, symbol = memoizeFunction, typeArgumentsCount = 1, - valueArgumentsCount = 3 + valueArgumentsCount = 4 ) .also { call -> call.patchDeclarationParents(function) } .apply { @@ -105,5 +121,6 @@ internal fun IrPluginContext.buildSaveInCacheCall( putValueArgument(0, keyLiteral) putValueArgument(1, argsListExpr) putValueArgument(2, lambdaExpr) + putValueArgument(3, qualifierArgs) } } \ No newline at end of file diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/ReadQualifierUtil.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/ReadQualifierUtil.kt new file mode 100644 index 0000000..90558f7 --- /dev/null +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/ReadQualifierUtil.kt @@ -0,0 +1,46 @@ +package io.github.stslex.compiler_plugin.utils + +import io.github.stslex.compiler_plugin.DistinctChangeConfig +import io.github.stslex.compiler_plugin.DistinctUntilChangeFun +import io.github.stslex.compiler_plugin.DistinctUntilChangeFun.Companion.LOGGING_DEFAULT +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.ir.builders.irBoolean +import org.jetbrains.kotlin.ir.builders.irCallConstructor +import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI +import org.jetbrains.kotlin.ir.util.constructors +import org.jetbrains.kotlin.ir.util.getAnnotation +import org.jetbrains.kotlin.ir.util.patchDeclarationParents +import org.jetbrains.kotlin.name.FqName + +@OptIn(UnsafeDuringIrConstructionAPI::class) +internal fun IrPluginContext.readQualifier( + function: IrSimpleFunction, + logger: CompileLogger +): IrExpression? { + val qualifiedName = DistinctUntilChangeFun::class.qualifiedName ?: return null + val annotation = function.getAnnotation(FqName(qualifiedName)) ?: return null + + logger.i("readQualifier: annotation is found for ${function.name}") + + val irBuilder = createIrBuilder(function) + + val currentValue = annotation.getValueArgument(0) + val logging = currentValue ?: irBuilder.irBoolean(LOGGING_DEFAULT) + + val constructorSymbol = referenceClass(DistinctChangeConfig::class.toClassId()) + ?.constructors + ?.firstOrNull() + ?: error("CheckChangesConfig not found in IR") + + return irBuilder + .irCallConstructor( + callee = constructorSymbol, + typeArguments = emptyList() + ) + .also { it.patchDeclarationParents(function) } + .apply { + putValueArgument(0, logging) + } +} \ No newline at end of file diff --git a/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/RuntimeLogger.kt b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/RuntimeLogger.kt new file mode 100644 index 0000000..66a5ac1 --- /dev/null +++ b/compiler-plugin/src/main/kotlin/io/github/stslex/compiler_plugin/utils/RuntimeLogger.kt @@ -0,0 +1,25 @@ +package io.github.stslex.compiler_plugin.utils + +import java.util.logging.Level +import java.util.logging.Logger + +internal class RuntimeLogger private constructor(tag: String) { + + private val logger = Logger.getLogger(tag) + + fun i(msg: String) { + logger.log(Level.INFO, msg) + } + + companion object { + + private const val TAG = "KotlinCompilerLogger" + + fun i(msg: String) { + RuntimeLogger(TAG).i(msg) + } + + fun tag(tag: String): RuntimeLogger = RuntimeLogger(tag) + } + +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2490567..ab40d40 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ activity = "1.10.0" constraintLayout = "2.2.0" jetbrainsKotlinJvm = "2.0.20" -stslexCompilerPlugin = "0.0.1" +stslexCompilerPlugin = "0.0.2" [libraries] android-desugarJdkLibs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" } diff --git a/settings.gradle.kts b/settings.gradle.kts index a120b22..b8f4794 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,7 +6,7 @@ pluginManagement { mavenCentral() // for getting plugin from local maven repository - // mavenLocal() + mavenLocal() } } @@ -16,10 +16,13 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + mavenLocal() } } rootProject.name = "CompilerPlugin" -include(":app") \ No newline at end of file +include(":app") +// for debug +//include(":compiler-plugin") \ No newline at end of file From df7c9f9f67495bfbec29ff83a545b6b91284483a Mon Sep 17 00:00:00 2001 From: stslex Date: Sun, 16 Feb 2025 17:19:15 +0300 Subject: [PATCH 2/2] fix packageArtifacts --- compiler-plugin/build.gradle.kts | 2 -- 1 file changed, 2 deletions(-) diff --git a/compiler-plugin/build.gradle.kts b/compiler-plugin/build.gradle.kts index 30224c0..5731913 100644 --- a/compiler-plugin/build.gradle.kts +++ b/compiler-plugin/build.gradle.kts @@ -129,9 +129,7 @@ tasks.register("packageArtifacts") { group = "publishing" description = "Create ZIP-archive with artifacts for Central Publisher Portal" - val localRepo = file(localRepoPath) - from(localRepo) println("📦 Package artifacts...")