diff --git a/build.gradle.kts b/build.gradle.kts index 9423f1a..c76fdf1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,9 +13,14 @@ group = "net.keinekekse" version = masterLibs.versions.versionPluginVersion.get() description = "Adds version information to the project." extra["displayname"] = "Version-Plugin" -extra["copyright"] = "Copyright (c) 2024-2025 Hartmut Bauer" +extra["copyright"] = "Copyright (c) 2024-2026 Hartmut Bauer" extra["buildTimestamp"] = Instant.now().toString() +dependencies { + implementation(masterLibs.semver) + compileOnly(masterLibs.kotlinGradlePlugin) +} + publishing { repositories { maven { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd49..f8e1ee3 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f853b..23449a2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a4..adff685 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -112,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -170,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -203,15 +203,14 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 25da30d..c4bdd3a 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -68,11 +70,10 @@ 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 %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/settings.gradle.kts b/settings.gradle.kts index 7dee989..91d82e7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,7 +7,7 @@ dependencyResolutionManagement { versionCatalogs { create("masterLibs") { - from("net.keinekekse:version-catalog:56") + from("net.keinekekse:version-catalog:104") } } } diff --git a/src/main/kotlin/net/keinekekse/version/AddVersionTask.kt b/src/main/kotlin/net/keinekekse/version/AddVersionTask.kt index b91d42f..85f4fa5 100644 --- a/src/main/kotlin/net/keinekekse/version/AddVersionTask.kt +++ b/src/main/kotlin/net/keinekekse/version/AddVersionTask.kt @@ -1,12 +1,134 @@ package net.keinekekse.version -import org.gradle.api.tasks.Copy -import org.gradle.api.tasks.TaskAction +import io.github.z4kn4fein.semver.Version +import io.github.z4kn4fein.semver.withoutSuffixes +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import java.util.* -abstract class AddVersionTask : Copy() { +abstract class AddVersionTask : DefaultTask() { + + // 1. Define what triggers the task (Source Files) + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val sourceFiles: ConfigurableFileCollection + + // 2. Define the output (The properties file) + @get:OutputFile + abstract val buildNumberFile: RegularFileProperty + + @get:OutputFile + abstract val versionClassFile: RegularFileProperty + + @get:Input + abstract val projectVersion: Property + + @get:Input + abstract val projectName: Property + + @get:Input + abstract val projectDescription: Property + + @get:Input + abstract val displayNameExtra: Property + + @get:Input + abstract val copyrightExtra: Property + + @get:Input + abstract val versionPackageName: Property @TaskAction - fun updateVersion() { - // action is defined in plugin + fun execute() { + println("execute task: incrementBuildNumber") + val buildNumber = incrementAndGetBuildNumber() + writeClassFile(buildNumber) } + + private fun writeClassFile(buildNumber: String) { + val ver = getVersionInfo(buildNumber) + + val file = versionClassFile.get().asFile + file.parentFile.mkdirs() + file.writeText( + """ + package ${versionPackageName.get()} + + object VERSION { + const val version = "${ver.version}" + const val fullVersion = "${ver.fullVersion}" + const val major = ${ver.major} + const val minor = ${ver.minor} + const val patch = ${ver.patch} + const val preRelease = "${ver.preRelease}" + const val buildMetadata = "${ver.buildMetadata}" + const val buildDate = "${ver.buildDate}" + const val displayName = "${ver.displayName}" + const val copyright = "${ver.copyright}" + const val name = "${ver.name}" + const val description = "${ver.description}" + } + """.trimIndent() + ) + + } + + private fun getVersionInfo(buildNumber: String): VersionInfo { + val version = + if (Version.parse(projectVersion.get(), strict = false).buildMetadata.isNullOrEmpty()) + Version.parse(projectVersion.get() + "+build.$buildNumber", strict = false) + else Version.parse(projectVersion.get(), strict = false) + + println("Source files changed. Setting version to $version") + + return VersionInfo( + major = version.major, + minor = version.minor, + patch = version.patch, + preRelease = version.preRelease ?: "", + buildMetadata = version.buildMetadata ?: "", + fullVersion = version.toString(), + version = if (version.isPreRelease) version.toString() else + version.withoutSuffixes().toString(), + buildDate = java.time.Instant.now().toString(), + displayName = displayNameExtra.get(), + copyright = copyrightExtra.get(), + name = projectName.get(), + description = projectDescription.get() ?: "" + ) + } + + private fun incrementAndGetBuildNumber(): String { + val file = buildNumberFile.get().asFile + val props = Properties() + if (file.exists()) { + file.inputStream().use { props.load(it) } + } + val current = props.getProperty("build.number")?.toLongOrNull() ?: 0 + val actual = (current + 1).toString() + props.setProperty("build.number", actual) + file.outputStream().use { + props.store(it, "auto generated") + } + return actual + } + + data class VersionInfo( + val version: String, + val fullVersion: String, + val major: Int, + val minor: Int, + val patch: Int, + val preRelease: String, + val buildMetadata: String, + val buildDate: String, + val displayName: String, + val copyright: String, + val name: String, + val description: String, + ) + } diff --git a/src/main/kotlin/net/keinekekse/version/VersionPlugin.kt b/src/main/kotlin/net/keinekekse/version/VersionPlugin.kt index eb2a264..8bbe6ea 100644 --- a/src/main/kotlin/net/keinekekse/version/VersionPlugin.kt +++ b/src/main/kotlin/net/keinekekse/version/VersionPlugin.kt @@ -1,131 +1,143 @@ package net.keinekekse.version +import org.gradle.api.InvalidUserDataException import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.file.RegularFile -import org.gradle.api.java.archives.Manifest -import org.gradle.api.tasks.bundling.Jar -import org.gradle.api.tasks.bundling.War -import org.gradle.kotlin.dsl.extra -import org.gradle.kotlin.dsl.named -import java.io.FileInputStream -import java.io.FileOutputStream -import java.time.Instant -import java.util.* +import org.gradle.api.tasks.SourceSetContainer +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +/** + * Versions-Resource im Projekt anlegen und bei jedem Build eine Build-Nummer hochzählen + * + * Ohne weitere Konfiguration wird die Datei Version.kt mit der class VERSION im Verzeichnis + * src/main/kotlin// erzeugt, für KMP in src/commonMain/kotlin. + * Die class wird mit Daten aus build.gradle.kts befüllt (s.u.). + * + * Für die Build-Nummer wird eine Datei buildnumber.properties im Projektverzeichnis erzeugt. + * + * Die class-Datei und der Package-Name können ggf. in build.gradle.kts konfiguriert werden: + * + * ``` + * plugins { + * // Plugin einbinden + * id("net.keinekekse.version") version "3.0.0" + * } + * + * // Werte, die für die Version-Datei verwendet werden + * // in settings.gradle.kt: rootProject.name = "program-name" + * group = "org.example" + * version = "0.0.1-beta.1" + * description = "Program description" + * extra["displayname"] = "Program" + * extra["copyright"] = "Copyright (c) 2025 Hartmut Bauer" + * + * + * // Optional: Konfiguration der Version-Datei und des Packages + * versionConfig { + * versionClassFile.set(layout.projectDirectory.file("src/main/kotlin/Version.kt")) + * versionPackageName.set("net.keinekekse") + * } + * ``` + * + * VERSION-class: + * + * ``` + * object VERSION { + * const val version = "1.0.0-beta.1+build.13" // für Release-Ver: major.minor.patch + * const val fullVersion = "1.0.0-beta.1+build.13" + * const val major = 1 + * const val minor = 0 + * const val patch = 0 + * const val preRelease = "beta.1" + * const val buildMetadata = "build.13" + * const val buildDate = "2026-01-10T18:54:42.227942Z" + * const val displayName = "Program" + * const val copyright = "Copyright (c) 2026 Hartmut Bauer" + * const val name = "program-name" + * const val description = "Program description" + * } + * ``` + * + * + */ class VersionPlugin : Plugin { - private var isWar = false - - companion object { - private const val BUILDNUMBER_FILE = "buildnumber.properties" - private const val BUILD_NUMBER_KEY = "build.number" - private const val GENERATED_COMMENT = "auto generated" - } override fun apply(project: Project) { - validateNoSubprojects(project) + val extension = + project.extensions.create("versionConfig", VersionPluginExtension::class.java) - project.afterEvaluate { - val projectInfo = ProjectInfo( - version = project.version.toString(), - displayName = getOptionalExtra(project, "displayname"), - copyright = getOptionalExtra(project, "copyright"), - name = project.name, - description = project.description ?: "" - ) + val versionTask = + project.tasks.register("incrementBuildNumber", AddVersionTask::class.java) { + if (!extension.versionClassFile.isPresent) { + val packageName = + project.group.toString().replace("-", ".") + + "." + project.name.replace("-", ".") + val versionFileName = packageName.replace(".", "/") + "/Version.kt" - isWar = isWarProject(project) - if (isWar) { - addManifestTask(project, "war", projectInfo) - } else { - addManifestTask(project, "jar", projectInfo) - } - } - } - - private fun validateNoSubprojects(project: Project) { - if (project.parent != null) { - throw Exception("This plugin cannot handle subprojects!") - } - } - - private fun isWarProject(project: Project): Boolean { - return project.layout.projectDirectory.file("src/main/webapp/WEB-INF").asFile.exists() - } - - private fun getOptionalExtra(project: Project, key: String): String { - return try { - project.extra[key]?.toString() ?: "" - } catch (e: Exception) { - "" - } - } - - private fun addManifestTask(project: Project, taskType: String, projectInfo: ProjectInfo) { - val countFile = project.layout.projectDirectory.file(BUILDNUMBER_FILE) - - if (taskType == "war") { - try { - project.tasks.named(taskType) { - doFirst { - applyManifestAttributes(manifest, countFile, projectInfo) + var targ = "src/main/kotlin/" + if (!project.layout.projectDirectory.file(targ).asFile.exists()) { + targ = "src/commonMain/kotlin/" + if (!project.layout.projectDirectory.file(targ).asFile.exists()) { + throw InvalidUserDataException( + "Version-Plugin: 'versionClassFile' and 'versionPackageName' must " + + "be set in a versionConfig-block in build.gradle.kts, " + + "as both 'src/main/kotlin' and 'src/commonMain/kotlin' do not exist!" + ) + } } + extension.versionClassFile.convention(project.layout.projectDirectory.file(targ + versionFileName)) + extension.versionPackageName.convention(packageName) } - } catch (e: Exception) { - println("\n*** Falsche Reihenfolge ***\n\nwar-Plugin muss vor dem Versions-Plugin eingetragen werden!\n") - throw e + + // Point to the source sets + val files = project.files( + "src/main/kotlin", "src/main/java", + "src/commonMain/kotlin", "src/commonMain/java", + "src/jvmMain/kotlin", "src/jvmMain/java" + ) + // Filter lazily using a provider + sourceFiles.from(extension.versionClassFile.map { classFile -> + files.asFileTree.filter { it.name != classFile.asFile.name } + }) + + // Point to the properties file + buildNumberFile.set(project.layout.projectDirectory.file("buildnumber.properties")) + + // Link properties directly (this is lazy) + versionClassFile.set(extension.versionClassFile) + versionPackageName.set(extension.versionPackageName) + + projectVersion.set(project.provider { project.version.toString() }) + projectName.set(project.provider { project.name }) + projectDescription.set(project.provider { project.description ?: "" }) + + displayNameExtra.set(project.provider { + project.extensions.extraProperties.let { + if (it.has("displayname")) it.get("displayname")?.toString() else "" + } + }) + copyrightExtra.set(project.provider { + project.extensions.extraProperties.let { + if (it.has("copyright")) it.get("copyright")?.toString() else "" + } + }) } - } else if (taskType == "jar") { - project.tasks.named(taskType) { - doFirst { - applyManifestAttributes(manifest, countFile, projectInfo) - } + + // 1. Handle Kotlin/JVM (Standard Project) + project.plugins.withId("org.jetbrains.kotlin.jvm") { + project.extensions.getByType(SourceSetContainer::class.java).named("main") { + // This tells Gradle that this source set includes the directory containing the generated file + this.java.srcDir(versionTask.map { it.versionClassFile.get().asFile.parentFile }) } } - } - private fun applyManifestAttributes( - manifest: Manifest, - countFile: RegularFile, - projectInfo: ProjectInfo, - ) { - val buildNumber = incrementBuildNumber(countFile) - - with(manifest.attributes) { - this["module-name"] = projectInfo.name - this["displayname"] = projectInfo.displayName - this["description"] = projectInfo.description - this["copyright"] = projectInfo.copyright - this["version"] = projectInfo.version - this["build-number"] = buildNumber - this["build-timestamp"] = Instant.now().toString() + // 2. Handle Kotlin Multiplatform + project.plugins.withId("org.jetbrains.kotlin.multiplatform") { + val kotlinExt = project.extensions.getByType(KotlinMultiplatformExtension::class.java) + kotlinExt.sourceSets.named("commonMain") { + this.kotlin.srcDir(versionTask.map { it.versionClassFile.get().asFile.parentFile }) + } } - println("build ${projectInfo.version}-$buildNumber") } - - private fun incrementBuildNumber(propFile: RegularFile): String { - val props = Properties() - try { - FileInputStream(propFile.asFile).use { props.load(it) } - } catch (e: Exception) { - // noop - } - - val currentBuildNumber = props.getProperty(BUILD_NUMBER_KEY)?.toLongOrNull() ?: 0 - val newBuildNumber = currentBuildNumber + 1 - - props.setProperty(BUILD_NUMBER_KEY, newBuildNumber.toString()) - props.store(FileOutputStream(propFile.asFile, false), GENERATED_COMMENT) - - return newBuildNumber.toString() - } - - data class ProjectInfo( - val version: String, - val displayName: String, - val copyright: String, - val name: String, - val description: String, - ) } diff --git a/src/main/kotlin/net/keinekekse/version/VersionPluginExtension.kt b/src/main/kotlin/net/keinekekse/version/VersionPluginExtension.kt index c517c3c..912a388 100644 --- a/src/main/kotlin/net/keinekekse/version/VersionPluginExtension.kt +++ b/src/main/kotlin/net/keinekekse/version/VersionPluginExtension.kt @@ -1,5 +1,10 @@ package net.keinekekse.version +import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property -interface VersionPluginExtension +interface VersionPluginExtension { + val versionPackageName: Property + + val versionClassFile: RegularFileProperty +}