Skip to content

Commit

Permalink
Add dedicated task for generating *.jar manifest
Browse files Browse the repository at this point in the history
  • Loading branch information
floscher committed Feb 6, 2022
1 parent ac94d5a commit c740ca3
Show file tree
Hide file tree
Showing 2 changed files with 212 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package org.openstreetmap.josm.gradle.plugin.config

import org.eclipse.jgit.api.Git
import org.gradle.api.Project
import org.gradle.api.provider.Property
import org.gradle.api.provider.SetProperty
import java.io.IOException
import java.net.URL
import java.time.Instant

/**
* The info that will be written into the manifest file of the plugin *.jar
Expand Down Expand Up @@ -202,6 +206,33 @@ class JosmManifest(private val project: Project) {
OSX, WINDOWS, UNIXOID
}

/**
* Provides an [Instant] that is supposed to reflect the point in time when the plugin was created.
* If available, we use the timestamp from version control, because that makes the build more reproducible,
* which amongst other things enables better build caching in Gradle. Also it's probably more meaningful to
* anyone reading the manifest, than the time the *.jar file was built from the sources.
*
* **Default value:** If you build in a git-repository, then the commit timestamp of the current `HEAD` commit is used.
* Otherwise the current time is used.
*
* **Influenced MANIFEST.MF attribute:** `Plugin-Date` ([Attribute.PLUGIN_DATE])
*
* @since 0.8.1
*/
public val pluginDate: Property<Instant> = project.objects.property(Instant::class.java)
.convention(
project.provider {
try {
Git.open(project.projectDir).repository.let { repo ->
Instant.ofEpochSecond(repo.parseCommit(repo.resolve("HEAD")).commitTime.toLong())
}
} catch (e: IOException) {
project.logger.warn("Failed to determine git committer timestamp. Falling back to the current time.")
Instant.now()
}
}
)

/**
* The name of a virtual plugin for which this plugin is a native implementation.
* Must be set in conjunction with [platform].
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package org.openstreetmap.josm.gradle.plugin.task;

import groovy.lang.GroovySystem
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.Directory
import org.gradle.api.file.RegularFile
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.CacheableTask
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.gradle.api.tasks.TaskProvider
import org.openstreetmap.josm.gradle.plugin.config.JosmManifest
import org.openstreetmap.josm.gradle.plugin.i18n.io.LangFileDecoder
import org.openstreetmap.josm.gradle.plugin.i18n.io.MsgId
import org.openstreetmap.josm.gradle.plugin.i18n.io.MsgStr
import org.openstreetmap.josm.gradle.plugin.util.josm
import java.io.File
import java.time.ZoneOffset
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.jar.Manifest
import javax.inject.Inject

@CacheableTask
public open class GenerateJarManifest @Inject constructor(
i18nCompileTask: TaskProvider<CompileToLang>
): DefaultTask() {
public companion object {
public const val MANIFEST_PATH: String = "META-INF/MANIFEST.MF"
}

@get: Input
public val descriptionTranslations: Provider<Map<String, String>> = i18nCompileTask.map {
val description = project.extensions.josm.manifest.description
if (description == null) {
mapOf()
} else {
val baseLangBytes: ByteArray? = it.outputDirectory.get().resolve("data/en.lang").takeIf { it.exists() }?.readBytes()
if (baseLangBytes == null) {
mapOf()
} else {
val otherLangFiles = it.outputDirectory.get().resolve("data")
.listFiles { file: File -> file.extension == "lang" }
?.associate { it.nameWithoutExtension to it.readBytes() }
?: mapOf()

LangFileDecoder.decodeMultipleLanguages("en", baseLangBytes, otherLangFiles)
.filterKeys { it != "en" }
.mapNotNull { (language, translations) ->
translations.entries
.firstOrNull { (key, _) ->
key == MsgId(MsgStr(description))
}
?.value
?.strings
?.singleOrNull()
?.let { JosmManifest.Attribute.pluginDescriptionKey(language) to it }
}.toMap()
}
}
}

@get:Input
public val predefinedAttributes: Provider<Map<String, String>> = project.provider {
val josmManifest = project.extensions.josm.manifest
val requiredFields = setOfNotNull(
RequiredAttribute.create(
JosmManifest.Attribute.PLUGIN_MIN_JOSM_VERSION,
josmManifest.minJosmVersion,
"the minimum JOSM version your plugin is compatible with"
),
RequiredAttribute.create(
JosmManifest.Attribute.PLUGIN_MAIN_CLASS,
josmManifest.mainClass,
"the full name of the main class of your plugin"
),
RequiredAttribute.create(
JosmManifest.Attribute.PLUGIN_DESCRIPTION,
josmManifest.description,
"the textual description of your plugin"
),
if (josmManifest.provides == null) null else {
RequiredAttribute.create(
JosmManifest.Attribute.PLUGIN_PLATFORM,
josmManifest.platform?.toString(),
"the platform for which this plugin is written (must be either Osx, Windows or Unixoid)"
)
},
if (josmManifest.platform == null) null else {
RequiredAttribute.create(
JosmManifest.Attribute.PLUGIN_PROVIDES,
josmManifest.provides,
"the name of the virtual plugin for which this is an implementation"
)
}
)

requiredFields.filterIsInstance<RequiredAttribute.Missing>()
.takeIf { it.isNotEmpty() }
?.let { missingRequiredFields ->
throw GradleException(
"""
|The JOSM plugin misses these required configuration options:
|${missingRequiredFields.joinToString("\n") { " * ${it.description}" }}
|You can add these lines to your `gradle.properties` file in order to fix this:
|${missingRequiredFields.joinToString("\n") { " * ${it.key.propertiesKey} = ‹${it.description}" }}
|Or alternatively add the equivalent lines to the `josm.manifest {}` block in your `build.gradle(.kts)` file.
""".trimMargin()
)
}

mapOf("Manifest-Version" to "1.0").plus(
requiredFields.filterIsInstance<RequiredAttribute.Present>().map { it.key to it.value }
.plus(
setOf<Pair<JosmManifest.Attribute, String?>>(
// Default attributes (always set)
JosmManifest.Attribute.CREATED_BY to "${System.getProperty("java.version")} (${System.getProperty("java.vendor")})",
JosmManifest.Attribute.GRADLE_VERSION to project.gradle.gradleVersion,
JosmManifest.Attribute.GROOVY_VERSION to GroovySystem.getVersion(),
JosmManifest.Attribute.PLUGIN_DATE to DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(
ZonedDateTime.ofInstant(project.extensions.josm.manifest.pluginDate.get(), ZoneOffset.UTC)
),
JosmManifest.Attribute.PLUGIN_VERSION to project.version.toString(),
JosmManifest.Attribute.PLUGIN_EARLY to josmManifest.loadEarly.toString(),
JosmManifest.Attribute.PLUGIN_CAN_LOAD_AT_RUNTIME to josmManifest.canLoadAtRuntime.toString(),

// Optional attributes (can be null)
JosmManifest.Attribute.AUTHOR to josmManifest.author,
JosmManifest.Attribute.CLASSPATH to josmManifest.classpath.joinToString(" ").takeIf { it.isNotBlank() },
JosmManifest.Attribute.PLUGIN_ICON to josmManifest.iconPath,
JosmManifest.Attribute.PLUGIN_LOAD_PRIORITY to josmManifest.loadPriority?.toString(),
JosmManifest.Attribute.PLUGIN_MIN_JAVA_VERSION to josmManifest.minJavaVersion?.toString(),
JosmManifest.Attribute.PLUGIN_PLATFORM to josmManifest.platform?.toString(),
JosmManifest.Attribute.PLUGIN_PROVIDES to josmManifest.provides,
JosmManifest.Attribute.PLUGIN_WEBSITE to josmManifest.website?.toString(),
JosmManifest.Attribute.PLUGIN_DEPENDENCIES to josmManifest.pluginDependencies.getOrElse(emptySet()).ifEmpty { null }?.joinToString(";")
)
)
.mapNotNull { (key, value) -> value?.let { key.manifestKey to it } }
.toMap()
// Add download links to older GitHub releases
.plus(if (josmManifest.includeLinksToGithubReleases) buildMapOfGitHubDownloadLinks(project) else mapOf())
.toSortedMap()
// Add manually specified links to older versions of the plugin
.plus(
josmManifest.oldVersionDownloadLinks
.associate { JosmManifest.Attribute.pluginDownloadLinkKey(it.minJosmVersion) to "${it.pluginVersion};${it.downloadURL}" }
.toSortedMap()
)
.plus(descriptionTranslations.get().toSortedMap())
)
}

@get:Internal
public val outputDirectory: Provider<Directory> = project.layout.buildDirectory.map { it.dir("josm-manifest") }

@get:OutputFile
public val outputFile: Provider<RegularFile> = outputDirectory.map { it.file(MANIFEST_PATH) }

@TaskAction
public fun action() {
Manifest().let { manifest ->
manifest.mainAttributes.putAll(predefinedAttributes.get())
outputFile.get().asFile.outputStream().use {
manifest.write(it)
}
}
}

private sealed class RequiredAttribute {
companion object {
fun create(key: JosmManifest.Attribute, value: String?, description: String) =
if (value == null) Missing(key, description) else Present(key, value)
}
data class Present(val key: JosmManifest.Attribute, val value: String): RequiredAttribute()
data class Missing(val key: JosmManifest.Attribute, val description: String): RequiredAttribute()
}
}

0 comments on commit c740ca3

Please sign in to comment.