Skip to content

Commit 8a0caf2

Browse files
authored
refactor: align plugin packaging with sample (#339)
Align `pluginZip` (renamed to installPlugin) task with the JetBrains Toolbox sample's Marketplace archive structure: extension.json remains at the archive root, while plugin resources and all shipped JARs live under <plugin-id>/ and <plugin-id>/lib/. This follows the sample's packaging convention, keeping the manifest at the archive root and grouping plugin files beneath the plugin ID. dependencies.json are also generated at build time from the full dependency-license report. The generated metadata is filtered against the resolved `runtimeClasspath`, so it includes every bundled direct and transitive library while excluding compile-only Toolbox APIs and other artifacts that are not shipped. Use the generated file for both local installation and ZIP packaging. Also added ZIP validation before publishing and after direct packaging. It verifies the manifest and resource layout, expected plugin and runtime JARs, JAR placement under <plugin-id>/lib/, and that Toolbox-provided dependencies are not bundled. In addition I renamed some of the build tasks to `generateExtensionJson`, `installPlugin`, and `packagePlugin` to match the JetBrains Toolbox sample. Remove the redundant `pluginPrettyZip` task, and update the README and CI build workflow to use the new task names.
1 parent 5134eeb commit 8a0caf2

11 files changed

Lines changed: 259 additions & 172 deletions

‎.github/workflows/build.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ jobs:
7575
7676
# Run plugin build
7777
- name: Run Build
78-
run: ./gradlew clean pluginZip --info
78+
run: ./gradlew clean packagePlugin --info
7979

8080
# Store already-built plugin as an artifact for downloading
8181
- name: Upload artifact

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Changed
6+
7+
- migrated plugin packaging to the new marketplace compatible structure
8+
59
## 0.9.1 - 2026-07-07
610

711
### Added

‎README.md‎

Lines changed: 62 additions & 67 deletions
Large diffs are not rendered by default.

‎build.gradle.kts‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import com.github.jk1.license.filter.ExcludeTransitiveDependenciesFilter
21
import com.github.jk1.license.render.JsonReportRenderer
32

43
plugins {
@@ -68,9 +67,6 @@ changelog {
6867

6968
licenseReport {
7069
renderers = arrayOf(JsonReportRenderer("dependencies.json"))
71-
filters = arrayOf(ExcludeTransitiveDependenciesFilter())
72-
// jq script to convert to our format:
73-
// `jq '[.dependencies[] | {name: .moduleName, version: .moduleVersion, url: .moduleUrl, license: .moduleLicense, licenseUrl: .moduleLicenseUrl}]' < build/reports/dependency-license/dependencies.json > src/main/resources/dependencies.json`
7470
}
7571

7672
tasks.test {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import com.fasterxml.jackson.databind.JsonNode
2+
import com.fasterxml.jackson.databind.SerializationFeature
3+
import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder
4+
import org.gradle.api.DefaultTask
5+
import org.gradle.api.file.ConfigurableFileCollection
6+
import org.gradle.api.file.RegularFileProperty
7+
import org.gradle.api.tasks.InputFile
8+
import org.gradle.api.tasks.InputFiles
9+
import org.gradle.api.tasks.OutputFile
10+
import org.gradle.api.tasks.TaskAction
11+
import kotlin.io.path.createDirectories
12+
13+
abstract class GenerateDependenciesJsonTask : DefaultTask() {
14+
15+
@get:InputFile
16+
abstract val licenseReportFile: RegularFileProperty
17+
18+
@get:OutputFile
19+
abstract val outputFile: RegularFileProperty
20+
21+
@get:InputFiles
22+
abstract val bundledDependencies: ConfigurableFileCollection
23+
24+
@TaskAction
25+
fun generate() {
26+
val mapper = jacksonMapperBuilder().enable(SerializationFeature.INDENT_OUTPUT).build()
27+
val bundledFileNames = bundledDependencies.files.map { it.name }.toSet()
28+
val reportDependencies = mapper.readTree(licenseReportFile.get().asFile)
29+
.path("dependencies")
30+
.map(::toPluginDependency)
31+
val dependencies = reportDependencies.filter { it.archiveFileName() in bundledFileNames }
32+
val unreportedDependencies = bundledFileNames - dependencies.map { it.archiveFileName() }.toSet()
33+
check(unreportedDependencies.isEmpty()) {
34+
"License report is missing bundled dependencies: ${unreportedDependencies.sorted().joinToString()}"
35+
}
36+
37+
outputFile.get().asFile.toPath().parent.createDirectories()
38+
mapper.writeValue(outputFile.get().asFile, dependencies)
39+
}
40+
41+
private fun toPluginDependency(dependency: JsonNode) =
42+
PluginDependency(
43+
name = dependency.path("moduleName").asText(),
44+
version = dependency.path("moduleVersion").asText(),
45+
url = dependency.nullableText("moduleUrl"),
46+
license = dependency.nullableText("moduleLicense"),
47+
licenseUrl = dependency.nullableText("moduleLicenseUrl"),
48+
)
49+
50+
private fun JsonNode.nullableText(fieldName: String): String? =
51+
path(fieldName).takeUnless { it.isMissingNode || it.isNull }?.asText()
52+
}
53+
54+
private data class PluginDependency(
55+
val name: String,
56+
val version: String,
57+
val url: String?,
58+
val license: String?,
59+
val licenseUrl: String?,
60+
) {
61+
fun archiveFileName() = "${name.substringAfter(':')}-$version.jar"
62+
}

‎buildSrc/src/main/kotlin/InstallToolboxPluginTask.kt‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import org.gradle.api.file.ConfigurableFileCollection
22
import org.gradle.api.file.DirectoryProperty
3+
import org.gradle.api.file.RegularFileProperty
34
import org.gradle.api.provider.Property
45
import org.gradle.api.tasks.Input
56
import org.gradle.api.tasks.InputDirectory
7+
import org.gradle.api.tasks.InputFile
68
import org.gradle.api.tasks.InputFiles
79
import org.gradle.api.tasks.Sync
810
import org.gradle.work.DisableCachingByDefault
@@ -16,6 +18,9 @@ abstract class InstallToolboxPluginTask : Sync() {
1618
@get:InputFiles
1719
abstract val extensionJsonFiles: ConfigurableFileCollection
1820

21+
@get:InputFile
22+
abstract val dependenciesFile: RegularFileProperty
23+
1924
@get:InputFiles
2025
abstract val runtimeDependencies: ConfigurableFileCollection
2126

@@ -28,8 +33,9 @@ abstract class InstallToolboxPluginTask : Sync() {
2833
init {
2934
from(jarFiles)
3035
from(extensionJsonFiles)
36+
from(dependenciesFile)
3137
from(resourcesDir) {
32-
include("dependencies.json", "icon.svg", "pluginIcon.svg")
38+
include("icon.svg", "pluginIcon.svg")
3339
}
3440
from(runtimeDependencies)
3541
into(extensionId.map { getPluginInstallDir(it).toFile() })

‎buildSrc/src/main/kotlin/ToolboxPluginZipTask.kt‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import org.gradle.api.file.ConfigurableFileCollection
22
import org.gradle.api.file.DirectoryProperty
3+
import org.gradle.api.file.RegularFileProperty
4+
import org.gradle.api.provider.Property
5+
import org.gradle.api.tasks.Input
36
import org.gradle.api.tasks.InputDirectory
7+
import org.gradle.api.tasks.InputFile
48
import org.gradle.api.tasks.InputFiles
59
import org.gradle.api.tasks.bundling.Zip
610

@@ -12,18 +16,33 @@ abstract class ToolboxPluginZipTask : Zip() {
1216
@get:InputFiles
1317
abstract val extensionJsonFiles: ConfigurableFileCollection
1418

19+
@get:InputFile
20+
abstract val dependenciesFile: RegularFileProperty
21+
1522
@get:InputFiles
1623
abstract val runtimeDependencies: ConfigurableFileCollection
1724

1825
@get:InputDirectory
1926
abstract val resourcesDir: DirectoryProperty
2027

28+
@get:Input
29+
abstract val pluginDirectory: Property<String>
30+
2131
init {
22-
from(jarFiles)
2332
from(extensionJsonFiles)
33+
from(dependenciesFile) {
34+
into(pluginDirectory)
35+
}
2436
from(resourcesDir) {
25-
include("dependencies.json", "icon.svg", "pluginIcon.svg")
37+
include("icon.svg", "pluginIcon.svg")
38+
into(pluginDirectory)
39+
}
40+
from(jarFiles) {
41+
into(pluginDirectory.map { "$it/lib" })
42+
}
43+
from(runtimeDependencies) {
44+
into(pluginDirectory.map { "$it/lib" })
2645
}
27-
from(runtimeDependencies)
46+
pluginDirectory.convention("")
2847
}
2948
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import org.gradle.api.DefaultTask
2+
import org.gradle.api.file.ConfigurableFileCollection
3+
import org.gradle.api.file.RegularFileProperty
4+
import org.gradle.api.provider.ListProperty
5+
import org.gradle.api.provider.Property
6+
import org.gradle.api.tasks.Input
7+
import org.gradle.api.tasks.InputFile
8+
import org.gradle.api.tasks.InputFiles
9+
import org.gradle.api.tasks.TaskAction
10+
import java.util.zip.ZipFile
11+
12+
abstract class ValidateToolboxPluginZipTask : DefaultTask() {
13+
14+
@get:InputFile
15+
abstract val pluginFile: RegularFileProperty
16+
17+
@get:Input
18+
abstract val extensionId: Property<String>
19+
20+
@get:InputFiles
21+
abstract val expectedLibraries: ConfigurableFileCollection
22+
23+
@get:Input
24+
abstract val toolboxProvidedDependencyNames: ListProperty<String>
25+
26+
@TaskAction
27+
fun validate() {
28+
val pluginDirectory = "${extensionId.get()}/"
29+
val expectedResources = setOf("dependencies.json", "icon.svg", "pluginIcon.svg")
30+
31+
ZipFile(pluginFile.get().asFile).use { zip ->
32+
val entries = zip.entries().asSequence().filterNot { it.isDirectory }.map { it.name }.toSet()
33+
val missingEntries = buildSet {
34+
if ("extension.json" !in entries) add("extension.json")
35+
expectedResources
36+
.map { "$pluginDirectory$it" }
37+
.filterNot(entries::contains)
38+
.forEach(::add)
39+
expectedLibraries.files
40+
.map { "$pluginDirectory" + "lib/${it.name}" }
41+
.filterNot(entries::contains)
42+
.forEach(::add)
43+
}
44+
check(missingEntries.isEmpty()) {
45+
"Plugin ZIP is missing required entries: ${missingEntries.sorted().joinToString()}"
46+
}
47+
48+
val rootEntries = entries.filterNot { '/' in it }.toSet()
49+
check(rootEntries == setOf("extension.json")) {
50+
"Plugin ZIP must contain only extension.json at its root; found: ${rootEntries.sorted().joinToString()}"
51+
}
52+
53+
val misplacedLibraries = entries.filter { it.endsWith(".jar") && !it.startsWith("${pluginDirectory}lib/") }
54+
check(misplacedLibraries.isEmpty()) {
55+
"Plugin ZIP libraries must be under ${pluginDirectory}lib: ${misplacedLibraries.sorted().joinToString()}"
56+
}
57+
58+
val bundledToolboxDependencies = entries.filter { entry ->
59+
entry.startsWith("${pluginDirectory}lib/") &&
60+
toolboxProvidedDependencyNames.get().any(entry::contains)
61+
}
62+
check(bundledToolboxDependencies.isEmpty()) {
63+
"Plugin ZIP must not bundle Toolbox-provided dependencies: ${bundledToolboxDependencies.sorted().joinToString()}"
64+
}
65+
}
66+
}
67+
}
Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
val toolbox = extensions.create<ToolboxExtension>("toolbox")
22

3-
val extensionJson by tasks.registering(GenerateExtensionJsonTask::class) {
3+
val generateExtensionJson by tasks.registering(GenerateExtensionJsonTask::class) {
44
extensionId.set(project.group.toString())
55
extensionVersion.set(project.version.toString())
66
apiVersion.set(toolbox.apiVersion)
@@ -13,52 +13,62 @@ val extensionJson by tasks.registering(GenerateExtensionJsonTask::class) {
1313

1414
tasks.named<Jar>("jar") {
1515
archiveBaseName.set(project.group.toString())
16-
dependsOn(extensionJson)
17-
from(extensionJson.flatMap { it.outputFile })
16+
dependsOn(generateExtensionJson)
17+
from(generateExtensionJson.flatMap { it.outputFile })
1818
}
1919

20-
val filteredDependencies = configurations.named("compileClasspath").map {
20+
val filteredDependencies = configurations.named("runtimeClasspath").map {
2121
filterToolboxProvidedDependencies(it.files)
2222
}
2323

24-
val copyPlugin by tasks.registering(InstallToolboxPluginTask::class) {
24+
val generatedDependenciesJson by tasks.registering(GenerateDependenciesJsonTask::class) {
2525
dependsOn(tasks.named("generateLicenseReport"))
26-
jarFiles.from(tasks.named<Jar>("jar"))
27-
extensionJsonFiles.from(extensionJson.flatMap { it.outputFile })
28-
runtimeDependencies.from(filteredDependencies)
29-
resourcesDir.set(layout.projectDirectory.dir("src/main/resources"))
30-
extensionId.set(project.group.toString())
26+
licenseReportFile.set(layout.buildDirectory.file("reports/dependency-license/dependencies.json"))
27+
outputFile.set(layout.buildDirectory.file("generated/dependencies.json"))
28+
bundledDependencies.from(filteredDependencies)
3129
}
3230

33-
/**
34-
* Useful when doing manual local installation.
35-
*/
36-
val pluginPrettyZip by tasks.registering(ToolboxPluginZipTask::class) {
37-
archiveBaseName.set(project.name)
38-
dependsOn(tasks.named("generateLicenseReport"))
31+
val installPlugin by tasks.registering(InstallToolboxPluginTask::class) {
32+
dependsOn(generatedDependenciesJson)
3933
jarFiles.from(tasks.named<Jar>("jar"))
40-
extensionJsonFiles.from(extensionJson.flatMap { it.outputFile })
34+
extensionJsonFiles.from(generateExtensionJson.flatMap { it.outputFile })
35+
dependenciesFile.set(generatedDependenciesJson.flatMap { it.outputFile })
4136
runtimeDependencies.from(filteredDependencies)
4237
resourcesDir.set(layout.projectDirectory.dir("src/main/resources"))
43-
into(project.group.toString()) // folder like com.coder.toolbox
38+
extensionId.set(project.group.toString())
4439
}
4540

46-
val pluginZip by tasks.registering(ToolboxPluginZipTask::class) {
47-
dependsOn(tasks.named("generateLicenseReport"))
41+
val packagePlugin by tasks.registering(ToolboxPluginZipTask::class) {
42+
dependsOn(generatedDependenciesJson)
4843
jarFiles.from(tasks.named<Jar>("jar"))
49-
extensionJsonFiles.from(extensionJson.flatMap { it.outputFile })
44+
extensionJsonFiles.from(generateExtensionJson.flatMap { it.outputFile })
45+
dependenciesFile.set(generatedDependenciesJson.flatMap { it.outputFile })
5046
runtimeDependencies.from(filteredDependencies)
5147
resourcesDir.set(layout.projectDirectory.dir("src/main/resources"))
48+
pluginDirectory.set(project.group.toString())
5249
archiveBaseName.set(project.group.toString())
5350
}
5451

52+
val validatePluginZip by tasks.registering(ValidateToolboxPluginZipTask::class) {
53+
dependsOn(packagePlugin)
54+
pluginFile.set(packagePlugin.flatMap { it.archiveFile })
55+
extensionId.set(project.group.toString())
56+
expectedLibraries.from(tasks.named<Jar>("jar"))
57+
expectedLibraries.from(filteredDependencies)
58+
toolboxProvidedDependencyNames.set(TOOLBOX_PROVIDED_DEPENDENCIES)
59+
}
60+
61+
packagePlugin.configure {
62+
finalizedBy(validatePluginZip)
63+
}
64+
5565
val cleanAll by tasks.registering(CleanAllTask::class) {
5666
dependsOn(tasks.named("clean"))
5767
extensionId.set(project.group.toString())
5868
}
5969

6070
val publishPlugin by tasks.registering(PublishToolboxPluginTask::class) {
61-
dependsOn(pluginZip)
71+
dependsOn(validatePluginZip)
6272
extensionId.set(project.group.toString())
63-
pluginFile.set(pluginZip.flatMap { it.archiveFile })
73+
pluginFile.set(packagePlugin.flatMap { it.archiveFile })
6474
}

‎gradle.properties‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
version=0.9.1
1+
version=0.9.2-alpha1
22
group=com.coder.toolbox
33
name=coder-toolbox

0 commit comments

Comments
 (0)