mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[fleet] Move fleet.bundles to Community
(cherry picked from commit c96573a42577eb7c277816db07337ca0183f369b) FLEET-MR-6894 GitOrigin-RevId: 2361ae5b2e6bd1b4c16c6f9a94fffef0f5d1f687
This commit is contained in:
committed by
intellij-monorepo-bot
parent
3b5dd30fd5
commit
e609b67cfd
Generated
+1
@@ -4,6 +4,7 @@
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/fleet/andel/fleet.andel.iml" filepath="$PROJECT_DIR$/fleet/andel/fleet.andel.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/fleet/bifurcan/fleet.bifurcan.iml" filepath="$PROJECT_DIR$/fleet/bifurcan/fleet.bifurcan.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/fleet/bundles/fleet.bundles.iml" filepath="$PROJECT_DIR$/community/fleet/bundles/fleet.bundles.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/fleet/compiler-plugins/fleet.compiler.plugins.iml" filepath="$PROJECT_DIR$/fleet/compiler-plugins/fleet.compiler.plugins.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/fleet/fastutil/fleet.fastutil.iml" filepath="$PROJECT_DIR$/fleet/fastutil/fleet.fastutil.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/fleet/junit4/fleet.junit4.iml" filepath="$PROJECT_DIR$/fleet/junit4/fleet.junit4.iml" />
|
||||
|
||||
@@ -221,6 +221,7 @@ commandInterface
|
||||
community-resources
|
||||
fleet/andel
|
||||
fleet/bifurcan
|
||||
fleet/bundles
|
||||
fleet/compiler-plugins
|
||||
fleet/fastutil
|
||||
fleet/junit4
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
### auto-generated section `build fleet.bundles` start
|
||||
load("//build:compiler-options.bzl", "create_kotlinc_options")
|
||||
load("@rules_jvm//:jvm.bzl", "jvm_library")
|
||||
|
||||
create_kotlinc_options(
|
||||
name = "custom_bundles",
|
||||
opt_in = [
|
||||
"kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"kotlin.ExperimentalStdlibApi",
|
||||
],
|
||||
x_consistent_data_class_copy_visibility = True,
|
||||
x_context_parameters = True,
|
||||
x_jvm_default = "all-compatibility",
|
||||
x_lambdas = "class"
|
||||
)
|
||||
|
||||
jvm_library(
|
||||
name = "bundles",
|
||||
module_name = "fleet.bundles",
|
||||
visibility = ["//visibility:public"],
|
||||
srcs = glob(["srcCommonMain/**/*.kt", "srcCommonMain/**/*.java", "srcCommonMain/**/*.form", "srcJvmMain/**/*.kt", "srcJvmMain/**/*.java", "srcJvmMain/**/*.form"], allow_empty = True, exclude = ["**/module-info.java"]),
|
||||
kotlinc_opts = ":custom_bundles",
|
||||
deps = [
|
||||
"@lib//:kotlin-stdlib",
|
||||
"@lib//:kotlinx-serialization-core",
|
||||
"@lib//:kotlinx-serialization-json",
|
||||
"@lib//:kotlinx-coroutines-core",
|
||||
"//fleet/util/core",
|
||||
"//fleet/modules/api",
|
||||
"//fleet/bifurcan",
|
||||
]
|
||||
)
|
||||
### auto-generated section `build fleet.bundles` end
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="kotlin-language" name="Kotlin">
|
||||
<configuration version="5" platform="JVM 21" allPlatforms="JVM [21]" useProjectSettings="false">
|
||||
<compilerSettings>
|
||||
<option name="additionalArguments" value="-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi -opt-in=kotlin.ExperimentalStdlibApi -Xlambdas=class -Xconsistent-data-class-copy-visibility -Xcontext-parameters -XXLanguage:+AllowEagerSupertypeAccessibilityChecks" />
|
||||
</compilerSettings>
|
||||
<compilerArguments>
|
||||
<stringArguments>
|
||||
<stringArg name="jvmTarget" arg="21" />
|
||||
<stringArg name="apiVersion" arg="2.2" />
|
||||
<stringArg name="languageVersion" arg="2.2" />
|
||||
</stringArguments>
|
||||
<arrayArguments>
|
||||
<arrayArg name="pluginClasspaths">
|
||||
<args>$KOTLIN_BUNDLED$/lib/kotlinx-serialization-compiler-plugin.jar</args>
|
||||
</arrayArg>
|
||||
<arrayArg name="pluginOptions" />
|
||||
</arrayArguments>
|
||||
</compilerArguments>
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/srcCommonMain" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/srcJvmMain" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/gradlebuild/build" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="kotlin-stdlib" level="project" />
|
||||
<orderEntry type="library" name="kotlinx-serialization-core" level="project" />
|
||||
<orderEntry type="library" name="kotlinx-serialization-json" level="project" />
|
||||
<orderEntry type="library" name="kotlinx-coroutines-core" level="project" />
|
||||
<orderEntry type="module" module-name="fleet.util.core" />
|
||||
<orderEntry type="module" module-name="fleet.modules.api" />
|
||||
<orderEntry type="module" module-name="fleet.bifurcan" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -0,0 +1,73 @@
|
||||
// IMPORT__MARKER_START
|
||||
import fleet.buildtool.conventions.configureAtMostOneJvmTargetOrThrow
|
||||
import fleet.buildtool.conventions.withJavaSourceSet
|
||||
// IMPORT__MARKER_END
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.multiplatform)
|
||||
id("fleet.project-module-conventions")
|
||||
id("fleet.toolchain-conventions")
|
||||
id("fleet.module-publishing-conventions")
|
||||
id("fleet.sdk-repositories-publishing-conventions")
|
||||
id("fleet.open-source-module-conventions")
|
||||
alias(libs.plugins.dokka)
|
||||
// GRADLE_PLUGINS__MARKER_START
|
||||
id("fleet-module")
|
||||
alias(jps.plugins.kotlin.serialization)
|
||||
// GRADLE_PLUGINS__MARKER_END
|
||||
}
|
||||
|
||||
fleetModule {
|
||||
module {
|
||||
name = "fleet.bundles"
|
||||
importedFromJps {}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||
kotlin {
|
||||
// KOTLIN__MARKER_START
|
||||
compilerOptions.freeCompilerArgs = listOf(
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlin.ExperimentalStdlibApi",
|
||||
"-Xlambdas=class",
|
||||
"-Xconsistent-data-class-copy-visibility",
|
||||
"-Xcontext-parameters",
|
||||
"-XXLanguage:+AllowEagerSupertypeAccessibilityChecks",
|
||||
)
|
||||
jvm {}
|
||||
wasmJs {
|
||||
browser {}
|
||||
}
|
||||
sourceSets.commonMain.configure { kotlin.srcDir(layout.projectDirectory.dir("../srcCommonMain")) }
|
||||
sourceSets.commonMain.configure { resources.srcDir(layout.projectDirectory.dir("../resourcesCommonMain")) }
|
||||
sourceSets.commonTest.configure { kotlin.srcDir(layout.projectDirectory.dir("../srcCommonTest")) }
|
||||
sourceSets.commonTest.configure { resources.srcDir(layout.projectDirectory.dir("../resourcesCommonTest")) }
|
||||
sourceSets.jvmMain.configure { kotlin.srcDir(layout.projectDirectory.dir("../srcJvmMain")) }
|
||||
configureAtMostOneJvmTargetOrThrow { compilations.named("main") { withJavaSourceSet { javaSourceSet -> javaSourceSet.java.srcDir(layout.projectDirectory.dir("../srcJvmMain")) } } }
|
||||
sourceSets.jvmMain.configure { resources.srcDir(layout.projectDirectory.dir("../resourcesJvmMain")) }
|
||||
sourceSets.jvmTest.configure { kotlin.srcDir(layout.projectDirectory.dir("../srcJvmTest")) }
|
||||
configureAtMostOneJvmTargetOrThrow { compilations.named("test") { withJavaSourceSet { javaSourceSet -> javaSourceSet.java.srcDir(layout.projectDirectory.dir("../srcJvmTest")) } } }
|
||||
sourceSets.jvmTest.configure { resources.srcDir(layout.projectDirectory.dir("../resourcesJvmTest")) }
|
||||
sourceSets.wasmJsMain.configure { kotlin.srcDir(layout.projectDirectory.dir("../srcWasmJsMain")) }
|
||||
sourceSets.wasmJsMain.configure { resources.srcDir(layout.projectDirectory.dir("../resourcesWasmJsMain")) }
|
||||
sourceSets.wasmJsTest.configure { kotlin.srcDir(layout.projectDirectory.dir("../srcWasmJsTest")) }
|
||||
sourceSets.wasmJsTest.configure { resources.srcDir(layout.projectDirectory.dir("../resourcesWasmJsTest")) }
|
||||
sourceSets.commonMain.dependencies {
|
||||
implementation(jps.org.jetbrains.kotlin.kotlin.stdlib1993400674.get().let { "${it.group}:${it.name}:${it.version}" }) {
|
||||
exclude(group = "org.jetbrains", module = "annotations")
|
||||
}
|
||||
implementation(jps.org.jetbrains.kotlinx.kotlinx.serialization.core.jvm1739247612.get().let { "${it.group}:kotlinx-serialization-core:${it.version}" }) {
|
||||
isTransitive = false
|
||||
}
|
||||
implementation(jps.org.jetbrains.kotlinx.kotlinx.serialization.json.jvm231489733.get().let { "${it.group}:kotlinx-serialization-json:${it.version}" }) {
|
||||
isTransitive = false
|
||||
}
|
||||
implementation(jps.com.intellij.platform.kotlinx.coroutines.core.jvm134738847.get().let { "${it.group}:kotlinx-coroutines-core:${it.version}" }) {
|
||||
isTransitive = false
|
||||
}
|
||||
implementation(project(":fleet.util.core"))
|
||||
implementation(project(":fleet.modules.api"))
|
||||
implementation(project(":fleet.bifurcan"))
|
||||
}
|
||||
// KOTLIN__MARKER_END
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package fleet.bundles
|
||||
|
||||
import fleet.util.Os
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Serializable
|
||||
enum class CoordinatesPlatform {
|
||||
WindowsX64,
|
||||
WindowsAarch64,
|
||||
LinuxX64,
|
||||
LinuxAarch64,
|
||||
MacOsX64,
|
||||
MacOsAarch64,
|
||||
Wasm
|
||||
}
|
||||
|
||||
val Coordinates.platforms: List<CoordinatesPlatform>?
|
||||
get() = meta[KnownCoordinatesMeta.Platforms]?.let {
|
||||
runCatching {
|
||||
Json.decodeFromString(ListSerializer(CoordinatesPlatform.serializer()), it)
|
||||
}.getOrDefault(emptyList())
|
||||
}
|
||||
|
||||
private val currentPlatform by lazy {
|
||||
if (Os.INSTANCE.isWasm) CoordinatesPlatform.Wasm
|
||||
else when (Os.INSTANCE.type) {
|
||||
Os.Type.Windows -> if (Os.INSTANCE.isAarch64) CoordinatesPlatform.WindowsAarch64 else CoordinatesPlatform.WindowsX64
|
||||
Os.Type.Linux -> if (Os.INSTANCE.isAarch64) CoordinatesPlatform.LinuxAarch64 else CoordinatesPlatform.LinuxX64
|
||||
Os.Type.MacOS -> if (Os.INSTANCE.isAarch64) CoordinatesPlatform.MacOsAarch64 else CoordinatesPlatform.MacOsX64
|
||||
Os.Type.Unknown -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun Iterable<Coordinates>.filterCoordinatesByPlatform(): List<Coordinates> = filter { coordinates ->
|
||||
coordinates.platforms?.contains(currentPlatform) != false
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package fleet.bundles
|
||||
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
class ResolutionException(coordinates: Coordinates, cause: Throwable? = null) : Exception("Can't resolve $coordinates", cause)
|
||||
|
||||
/**
|
||||
* Responsible for downloading jars, checking their hashes, and caching
|
||||
* @throws ResolutionException
|
||||
*/
|
||||
interface CoordinatesResolution {
|
||||
suspend fun resolveFile(coordinates: Coordinates): ResolvedFile
|
||||
suspend fun resolvePluginPartsCoordinates(coordinates: Coordinates): PluginParts
|
||||
suspend fun resolveModule(coordinates: ModuleCoordinates): ModuleOnDisc
|
||||
suspend fun resolveResource(coordinates: Coordinates): ResourceBundle
|
||||
}
|
||||
|
||||
@JvmInline
|
||||
value class ResolvedFile(val path: String)
|
||||
|
||||
suspend fun CoordinatesResolution.resolve(layer: PluginLayer): ResolvedPluginLayer =
|
||||
ResolvedPluginLayer(modules = layer.modules,
|
||||
modulePath = layer.modulePath.map { moduleCoords ->
|
||||
resolveModule(moduleCoords)
|
||||
}.toSet(),
|
||||
resources = layer.resources.filterCoordinatesByPlatform().map {
|
||||
resolveResource(it)
|
||||
}.toSet())
|
||||
|
||||
suspend fun CoordinatesResolution.resolveParts(descriptor: PluginDescriptor): PluginParts {
|
||||
val coordinates = requireNotNull(descriptor.partsCoordinates) {
|
||||
"missing parts coordinates in descriptor: $descriptor"
|
||||
}
|
||||
return resolvePluginPartsCoordinates(coordinates)
|
||||
}
|
||||
|
||||
data class ResolvedPluginLayer(val modulePath: Set<ModuleOnDisc>,
|
||||
val modules: Set<String>,
|
||||
val resources: Set<ResourceBundle>)
|
||||
|
||||
data class ModuleOnDisc(val path: String,
|
||||
val serializedModuleDescriptor: String?)
|
||||
|
||||
fun interface ResourceBundle {
|
||||
operator fun get(key: String): ByteArray?
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package fleet.bundles
|
||||
|
||||
private fun deps(vararg pairs: Pair<LayerSelector, LayerSelector>): Map<LayerSelector, List<LayerSelector>> =
|
||||
pairs.groupBy(keySelector = { (dependant, _) -> dependant }, valueTransform = { (_, dependency) -> dependency })
|
||||
|
||||
val dockLayer = LayerSelector("dock")
|
||||
val frontendApiLayer = LayerSelector("frontendApi")
|
||||
val frontendImplLayer = LayerSelector("frontendImpl")
|
||||
@Deprecated(message = "use `frontendImpl` instead", replaceWith = ReplaceWith("frontendImplLayer"))
|
||||
val frontendLayer = LayerSelector("frontend")
|
||||
val workspaceApiLayer = LayerSelector("workspaceApi")
|
||||
val workspaceImplLayer = LayerSelector("workspaceImpl")
|
||||
@Deprecated(message = "use `workspaceImpl` instead", replaceWith = ReplaceWith("workspaceImpl"))
|
||||
val workspaceLayer = LayerSelector("workspace")
|
||||
val commonApiLayer = LayerSelector("commonApi")
|
||||
val commonImplLayer = LayerSelector("commonImpl")
|
||||
@Deprecated(message = "use `commonImpl` instead", replaceWith = ReplaceWith("commonImpl"))
|
||||
val commonLayer = LayerSelector("common")
|
||||
val testLayer = LayerSelector("test")
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val deprecatedLayers = listOf(
|
||||
frontendLayer,
|
||||
commonLayer,
|
||||
workspaceLayer,
|
||||
)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
//this describes dependencies between layers inside a plugin
|
||||
val internalReadability: Map<LayerSelector, List<LayerSelector>> = deps(
|
||||
frontendImplLayer to frontendApiLayer,
|
||||
frontendImplLayer to commonImplLayer,
|
||||
frontendImplLayer to commonApiLayer,
|
||||
frontendImplLayer to dockLayer,
|
||||
frontendApiLayer to commonApiLayer,
|
||||
workspaceImplLayer to workspaceApiLayer,
|
||||
workspaceImplLayer to commonApiLayer,
|
||||
workspaceImplLayer to commonImplLayer,
|
||||
workspaceApiLayer to commonApiLayer,
|
||||
frontendLayer to commonLayer,
|
||||
frontendLayer to dockLayer,
|
||||
workspaceLayer to commonLayer
|
||||
)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
//this describes dependencies between plugins
|
||||
val externalReadability: Map<LayerSelector, List<LayerSelector>> = deps(
|
||||
frontendImplLayer to frontendApiLayer,
|
||||
frontendImplLayer to commonApiLayer,
|
||||
frontendApiLayer to frontendApiLayer,
|
||||
frontendApiLayer to commonApiLayer,
|
||||
|
||||
workspaceImplLayer to workspaceApiLayer,
|
||||
workspaceImplLayer to commonApiLayer,
|
||||
workspaceApiLayer to workspaceApiLayer,
|
||||
workspaceApiLayer to commonApiLayer,
|
||||
|
||||
commonApiLayer to commonApiLayer,
|
||||
commonImplLayer to commonApiLayer,
|
||||
|
||||
//working with old plugins (to be removed)
|
||||
workspaceApiLayer to workspaceLayer,
|
||||
workspaceImplLayer to workspaceLayer,
|
||||
workspaceLayer to workspaceApiLayer,
|
||||
|
||||
workspaceLayer to workspaceLayer,
|
||||
frontendApiLayer to frontendLayer,
|
||||
frontendImplLayer to frontendLayer,
|
||||
frontendLayer to frontendApiLayer,
|
||||
frontendLayer to frontendLayer,
|
||||
frontendLayer to commonLayer,
|
||||
frontendLayer to dockLayer,
|
||||
commonLayer to dockLayer,
|
||||
workspaceLayer to commonLayer,
|
||||
commonLayer to commonLayer,
|
||||
frontendLayer to commonApiLayer,
|
||||
workspaceLayer to commonApiLayer,
|
||||
commonLayer to commonApiLayer,
|
||||
commonApiLayer to commonLayer,
|
||||
commonImplLayer to commonLayer,
|
||||
dockLayer to dockLayer,
|
||||
)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val frontendLayers: Set<LayerSelector> = setOf(
|
||||
dockLayer,
|
||||
frontendApiLayer,
|
||||
frontendImplLayer,
|
||||
commonApiLayer,
|
||||
commonImplLayer,
|
||||
frontendLayer,
|
||||
commonLayer,
|
||||
)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val workspaceLayers: Set<LayerSelector> = setOf(
|
||||
dockLayer,
|
||||
workspaceApiLayer,
|
||||
workspaceImplLayer,
|
||||
commonApiLayer,
|
||||
commonImplLayer,
|
||||
workspaceLayer,
|
||||
commonLayer,
|
||||
)
|
||||
|
||||
fun Collection<LayerSelector>.sortByInternalDependencies(): List<LayerSelector> {
|
||||
val currentStack = mutableListOf<LayerSelector>()
|
||||
|
||||
fun logCycle(): Nothing {
|
||||
currentStack.reverse()
|
||||
val endCycleIndex = currentStack.lastIndexOf(currentStack.first())
|
||||
throw IllegalArgumentException(
|
||||
"Cyclic dependency: ["
|
||||
+ currentStack.subList(0, endCycleIndex + 1).joinToString(separator = " <- ") { it.selector }
|
||||
+ "] <- "
|
||||
+ currentStack.subList(endCycleIndex + 1, currentStack.size).joinToString(separator = " <- ") { it.selector })
|
||||
}
|
||||
|
||||
val times = HashMap<LayerSelector, Int>()
|
||||
var timestamp = 0
|
||||
fun dfs(selector: LayerSelector) {
|
||||
when (times[selector]) {
|
||||
null -> {
|
||||
currentStack.add(selector)
|
||||
times[selector] = -1
|
||||
internalReadability[selector]?.forEach { dependency ->
|
||||
dfs(dependency)
|
||||
}
|
||||
times[selector] = timestamp++
|
||||
currentStack.removeLast()
|
||||
}
|
||||
-1 -> {
|
||||
currentStack.add(selector)
|
||||
logCycle()
|
||||
}
|
||||
}
|
||||
}
|
||||
forEach { dfs(it) }
|
||||
return times.entries.sortedBy { it.value }.map { it.key }
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package fleet.bundles
|
||||
|
||||
/**
|
||||
* Could be backed by Marketplace, or Maven repository
|
||||
* Subject for trust??
|
||||
**/
|
||||
interface PluginRepository {
|
||||
companion object {
|
||||
val Empty = object : PluginRepository {
|
||||
override suspend fun getLatestVersions(names: Set<PluginName>, shipVersion: PluginVersion): Map<PluginName, PluginVersion> = emptyMap()
|
||||
override suspend fun getPlugin(pluginName: PluginName, pluginVersion: PluginVersion): PluginDescriptor? = null
|
||||
override fun presentableName(): String = "Empty PluginRepository"
|
||||
override fun cacheKey(): String = "empty" // we should not attempt to re-resolve against the Empty repository if we already have a resolution result against it, it will lead to the same result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should check signature of [fleet.bundles.PluginDescriptor] before returning it
|
||||
**/
|
||||
suspend fun getLatestVersions(names: Set<PluginName>, shipVersion: PluginVersion): Map<PluginName, PluginVersion>
|
||||
suspend fun getPlugin(pluginName: PluginName, pluginVersion: PluginVersion): PluginDescriptor?
|
||||
|
||||
fun presentableName() : String = toString()
|
||||
|
||||
/**
|
||||
* Cache invalidation looks at this identifier to know whether a resolution against that repository is stale or not.
|
||||
*/
|
||||
fun cacheKey() : String
|
||||
}
|
||||
|
||||
fun PluginSet.asPluginRepository(): PluginRepository {
|
||||
return object : PluginRepository {
|
||||
override suspend fun getLatestVersions(names: Set<PluginName>, shipVersion: PluginVersion): Map<PluginName, PluginVersion> =
|
||||
plugins.groupBy(PluginDescriptor::name).mapNotNull { (name, bs) ->
|
||||
bs.filter {
|
||||
it.compatibleShipVersionRange?.let { range ->
|
||||
shipVersion in range.from..range.to
|
||||
} ?: false
|
||||
}
|
||||
.maxByOrNull { it.version }
|
||||
?.let { it.name to it.version }
|
||||
}.toMap()
|
||||
|
||||
override suspend fun getPlugin(pluginName: PluginName, pluginVersion: PluginVersion): PluginDescriptor? =
|
||||
plugins.find { spec -> spec.name == pluginName && spec.version == pluginVersion }
|
||||
|
||||
override fun presentableName(): String = "Plugin Repository from plugins set with ${this@asPluginRepository.plugins.size} plugins"
|
||||
override fun cacheKey(): String = buildString {
|
||||
shipVersions.sorted().forEach { shipVersion ->
|
||||
append(shipVersion)
|
||||
}
|
||||
plugins.sortedBy { descriptor -> descriptor.name.name }.forEach { descriptor ->
|
||||
append(descriptor.name.name)
|
||||
append(descriptor.version.versionString)
|
||||
descriptor.deps.entries.sortedBy { (name, _) -> name.name }.forEach { (name, version) ->
|
||||
append(name.name)
|
||||
append(version.version.versionString)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun PluginRepository.compose(other: PluginRepository): PluginRepository {
|
||||
val self = this
|
||||
return object : PluginRepository {
|
||||
override suspend fun getLatestVersions(names: Set<PluginName>, shipVersion: PluginVersion): Map<PluginName, PluginVersion> =
|
||||
self.getLatestVersions(names, shipVersion).merge(other.getLatestVersions(names, shipVersion)) { _, v1, v2 ->
|
||||
max(v1, v2)
|
||||
}
|
||||
|
||||
override suspend fun getPlugin(pluginName: PluginName, pluginVersion: PluginVersion): PluginDescriptor? {
|
||||
//TODO[jetzajac]: ambigs?
|
||||
return other.getPlugin(pluginName, pluginVersion) ?: self.getPlugin(pluginName, pluginVersion)
|
||||
}
|
||||
|
||||
override fun presentableName(): String {
|
||||
return "Composed PluginRepository: ${self.presentableName()} -> ${other.presentableName()}"
|
||||
}
|
||||
|
||||
override fun cacheKey(): String = "${self.cacheKey()}-${other.cacheKey()}"
|
||||
}
|
||||
}
|
||||
|
||||
private fun<K, V> Map<K, V>.merge(other: Map<K, V>, f: (K, V, V) -> V = {_, _, v -> v}): Map<K, V> {
|
||||
val x = toMutableMap()
|
||||
other.forEach { (k, v) ->
|
||||
when {
|
||||
x.containsKey(k) -> {
|
||||
x[k] = f(k, x[k]!! as V, v)
|
||||
}
|
||||
else -> x[k] = v
|
||||
}
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
private fun <T: Comparable<T>> max(c1: T, c2: T): T {
|
||||
val x = c1.compareTo(c2)
|
||||
return when {
|
||||
x == 0 -> c1
|
||||
x < 0 -> c2
|
||||
else -> c1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
package fleet.bundles
|
||||
|
||||
import fleet.util.async.catching
|
||||
import fleet.util.letIf
|
||||
import fleet.util.logging.logger
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Serializable
|
||||
sealed class PluginCommand {
|
||||
@Serializable
|
||||
@SerialName("add")
|
||||
data class Add(val pluginName: PluginName, val exactVersion: PluginVersion? = null) : PluginCommand()
|
||||
|
||||
@SerialName("remove")
|
||||
@Serializable
|
||||
data class Remove(val pluginName: PluginName) : PluginCommand()
|
||||
}
|
||||
|
||||
data class RequestedPluginConfiguration(
|
||||
val requestedVersions: Map<PluginName, PluginVersion?>,
|
||||
val forbidInstallation: Set<PluginName>
|
||||
) {
|
||||
companion object {
|
||||
val EMPTY = RequestedPluginConfiguration(emptyMap(), emptySet())
|
||||
}
|
||||
}
|
||||
|
||||
private val log by lazy { logger<PluginsConfig>() }
|
||||
|
||||
/**
|
||||
* collected from settings
|
||||
* */
|
||||
data class PluginsConfig(val commands: List<PluginCommand>) {
|
||||
companion object
|
||||
}
|
||||
|
||||
data class PluginResolutionResult(val config: ResolvedPluginsConfig,
|
||||
val problems: List<Problem>) {
|
||||
sealed class Problem {
|
||||
data class PluginNotFound(val name: PluginName) : Problem()
|
||||
data class Conflict(val pluginName: PluginName,
|
||||
val dependencyName: PluginName,
|
||||
val takenVersion: PluginVersion,
|
||||
val requiredVersion: VersionRequirement) : Problem()
|
||||
data class FetchIssue(val pluginName: PluginName,
|
||||
val throwable: Throwable) : Problem()
|
||||
}
|
||||
}
|
||||
|
||||
private data class FrontResolutionArguments(
|
||||
val alreadyResolvedPlugins: Map<PluginName, PluginDescriptor>,
|
||||
val front: Map<PluginName, PluginVersion>
|
||||
)
|
||||
|
||||
private data class FrontResolutionResult(
|
||||
val resolvedPlugins: Map<PluginName, PluginDescriptor>,
|
||||
val nextFront: Map<PluginName, PluginVersion>,
|
||||
val problemsFound: List<PluginResolutionResult.Problem>
|
||||
)
|
||||
|
||||
private typealias FrontResolution = suspend (FrontResolutionArguments) -> FrontResolutionResult
|
||||
|
||||
/**
|
||||
* Resolution algorithm. Resolves target plugins layer by layer using provided `frontResolution` lambda.
|
||||
* @param frontResolution lambda to resolve the current layer and define the next one.
|
||||
* @param concretedPlugins additional fixed plugin set used as a base layer to satisfy dependencies.
|
||||
* @return `PluginResolutionResult` with all the plugins which dependencies were satisfied
|
||||
*/
|
||||
private suspend fun resolvePluginsConfig(
|
||||
pluginsConfig: PluginsConfig,
|
||||
shipVersion: PluginVersion,
|
||||
repo: PluginRepository,
|
||||
concretedPlugins: Map<PluginName, PluginDescriptor> = emptyMap(),
|
||||
frontResolution: FrontResolution
|
||||
): PluginResolutionResult {
|
||||
val problems = ArrayList<PluginResolutionResult.Problem>()
|
||||
val requestedPluginConfiguration = calculateRequestedPlugins(pluginsConfig)
|
||||
val requestedPlugins = requestedPluginConfiguration.requestedVersions
|
||||
val forbidInstallation = requestedPluginConfiguration.forbidInstallation
|
||||
val pluginsWithVersionRequirement = requestedPlugins.mapNotNullTo(HashSet()) { (name, version) ->
|
||||
if (version != null) name else null
|
||||
}
|
||||
val pluginsWithoutVersionRequirement = requestedPlugins.mapNotNullTo(HashSet()) { (name, version) ->
|
||||
if (version == null) name else null
|
||||
}
|
||||
val latestVersions = repo.getLatestVersions(pluginsWithoutVersionRequirement, shipVersion)
|
||||
|
||||
problems.addAll(pluginsWithoutVersionRequirement
|
||||
.filter { name -> !latestVersions.containsKey(name) }
|
||||
.map(PluginResolutionResult.Problem::PluginNotFound))
|
||||
|
||||
val requestedPluginsWithVersions =
|
||||
(pluginsWithVersionRequirement.map { name ->
|
||||
name to requestedPlugins[name]!!
|
||||
} +
|
||||
pluginsWithoutVersionRequirement.mapNotNull { name ->
|
||||
latestVersions[name]?.let { version -> name to version }
|
||||
}).toMap()
|
||||
|
||||
val pluginsToLoad = run {
|
||||
val resolvedPlugins = hashMapOf<PluginName, PluginDescriptor>()
|
||||
var front = requestedPluginsWithVersions
|
||||
while (front.isNotEmpty()) {
|
||||
front = front.filterNot { forbidInstallation.contains(it.key) } // the whole subtree will be dropped, no reason to resolve it
|
||||
val arguments = FrontResolutionArguments(resolvedPlugins, front)
|
||||
val result = frontResolution(arguments)
|
||||
resolvedPlugins.putAll(result.resolvedPlugins)
|
||||
problems.addAll(result.problemsFound)
|
||||
front = result.nextFront
|
||||
}
|
||||
filterMissingPluginsOut(resolvedPlugins, concretedPlugins, requestedPluginsWithVersions.keys)
|
||||
}
|
||||
|
||||
return PluginResolutionResult(ResolvedPluginsConfig(bundlesToLoad = pluginsToLoad.values.toSet(),
|
||||
shipVersion = shipVersion),
|
||||
problems = problems)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves plugins from provided `PluginsConfig` using the provided `PluginRepository`.
|
||||
*
|
||||
* @param ignoreFrontendOnly - to filter out frontend-only plugins, such as themes, keymaps or icon packs during resolution
|
||||
* @return PluginResolutionResult with all the plugins which dependencies were satisfied
|
||||
*/
|
||||
suspend fun resolveWorkspacePlugins(
|
||||
pluginsConfig: PluginsConfig,
|
||||
shipVersion: PluginVersion,
|
||||
repo: PluginRepository,
|
||||
ignoreFrontendOnly: Boolean = true,
|
||||
): PluginResolutionResult {
|
||||
return resolvePluginsConfig(pluginsConfig, shipVersion, repo) { arguments ->
|
||||
val problems = ArrayList<PluginResolutionResult.Problem>()
|
||||
val pluginsByNamePrime = repo.getPlugins(arguments.front).let { fetchingResult ->
|
||||
problems.addAll(fetchingResult.problems)
|
||||
fetchingResult.descriptors.letIf(ignoreFrontendOnly) { ds ->
|
||||
ds.filterNot { it.value.meta[KnownMeta.FrontendOnly] == "true" }
|
||||
}
|
||||
}
|
||||
val conflicts: Map<PluginName, List<PluginVersion>> = pluginsByNamePrime
|
||||
.flatMap { (pluginName, plugin) ->
|
||||
plugin.deps.mapNotNull { (dependencyName, versionRequirement) ->
|
||||
val satisfied = arguments.alreadyResolvedPlugins[dependencyName]
|
||||
when {
|
||||
satisfied == null -> dependencyName to versionRequirement.version
|
||||
satisfied.version.satisfies(versionRequirement) -> null
|
||||
else -> {
|
||||
//TODO: try to satisfy existing dependencies with the required version???
|
||||
problems.add(PluginResolutionResult.Problem.Conflict(pluginName = pluginName,
|
||||
dependencyName = dependencyName,
|
||||
takenVersion = satisfied.version,
|
||||
requiredVersion = versionRequirement))
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.groupBy(keySelector = { (name, _) -> name },
|
||||
valueTransform = { (_, requirement) -> requirement })
|
||||
|
||||
val nextDeps = conflicts.map { (name, requirements) ->
|
||||
//TODO: handle conflicts better
|
||||
name to requirements.max()
|
||||
}.toMap()
|
||||
|
||||
FrontResolutionResult(resolvedPlugins = pluginsByNamePrime,
|
||||
nextFront = nextDeps,
|
||||
problemsFound = problems)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resolves frontend only plugins from provided config, using workspaceResolvedConfig as a base layer.
|
||||
*
|
||||
* @return only frontend-only plugins, which dependencies were satisfied by the workspaceResolvedConfig
|
||||
*/
|
||||
suspend fun resolveFrontendOnlyPlugins(
|
||||
pluginsConfig: PluginsConfig,
|
||||
shipVersion: PluginVersion,
|
||||
repo: PluginRepository,
|
||||
workspaceResolvedConfig: ResolvedPluginsConfig
|
||||
): PluginResolutionResult {
|
||||
val concretedBaseLayer = workspaceResolvedConfig.bundlesToLoad.associateBy { it.name }
|
||||
return resolvePluginsConfig(pluginsConfig, shipVersion, repo, concretedBaseLayer) { arguments ->
|
||||
val problems = ArrayList<PluginResolutionResult.Problem>()
|
||||
val pluginsByNamePrime = repo.getPlugins(arguments.front)
|
||||
.let { fetchingResult ->
|
||||
problems.addAll(fetchingResult.problems)
|
||||
fetchingResult.descriptors
|
||||
}
|
||||
.filterValues { it.meta[KnownMeta.FrontendOnly] == "true" }
|
||||
val conflicts: Map<PluginName, List<PluginVersion>> = pluginsByNamePrime
|
||||
.flatMap { (pluginName, plugin) ->
|
||||
plugin.deps.mapNotNull { (dependencyName, versionRequirement) ->
|
||||
val satisfiedByWorkspace = concretedBaseLayer[dependencyName]
|
||||
val satisfied = arguments.alreadyResolvedPlugins[dependencyName]
|
||||
val frontendOnly = plugin.meta[KnownMeta.FrontendOnly] == "true"
|
||||
if (frontendOnly) when {
|
||||
satisfied == null -> dependencyName to versionRequirement.version
|
||||
satisfied.version.satisfies(versionRequirement) -> null
|
||||
else -> {
|
||||
// TODO: try to satisfy existing dependencies with the required version???
|
||||
problems.add(PluginResolutionResult.Problem.Conflict(pluginName = pluginName,
|
||||
dependencyName = dependencyName,
|
||||
takenVersion = satisfied.version,
|
||||
requiredVersion = versionRequirement))
|
||||
null
|
||||
}
|
||||
}
|
||||
else {
|
||||
when {
|
||||
satisfiedByWorkspace == null -> {
|
||||
problems.add(PluginResolutionResult.Problem.PluginNotFound(pluginName))
|
||||
}
|
||||
!satisfiedByWorkspace.version.satisfies(versionRequirement) -> {
|
||||
problems.add(PluginResolutionResult.Problem.Conflict(pluginName = pluginName,
|
||||
dependencyName = dependencyName,
|
||||
takenVersion = satisfiedByWorkspace.version,
|
||||
requiredVersion = versionRequirement))
|
||||
}
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
.groupBy(keySelector = { (name, _) -> name },
|
||||
valueTransform = { (_, requirement) -> requirement })
|
||||
|
||||
val nextDeps = conflicts.map { (name, requirements) ->
|
||||
//TODO: handle conflicts better
|
||||
name to requirements.max()
|
||||
}.toMap()
|
||||
|
||||
FrontResolutionResult(resolvedPlugins = pluginsByNamePrime,
|
||||
nextFront = nextDeps,
|
||||
problemsFound = problems)
|
||||
}
|
||||
}
|
||||
|
||||
fun calculateRequestedPlugins(pluginsConfig: PluginsConfig): RequestedPluginConfiguration {
|
||||
val versionsToInstall = mutableMapOf<PluginName, PluginVersion?>()
|
||||
val forbidInstallation = mutableSetOf<PluginName>()
|
||||
|
||||
pluginsConfig.commands.forEach { command ->
|
||||
when (command) {
|
||||
is PluginCommand.Add -> versionsToInstall[command.pluginName] = command.exactVersion
|
||||
is PluginCommand.Remove -> {
|
||||
versionsToInstall.remove(command.pluginName)
|
||||
forbidInstallation.add(command.pluginName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RequestedPluginConfiguration(versionsToInstall, forbidInstallation)
|
||||
}
|
||||
|
||||
private fun filterMissingPluginsOut(resolvedOnes: Map<PluginName, PluginDescriptor>,
|
||||
concretedPlugins: Map<PluginName, PluginDescriptor>,
|
||||
requestedPlugins: Set<PluginName>): Map<PluginName, PluginDescriptor> {
|
||||
fun walkSubtree(resolvedOnes: Map<PluginName, PluginDescriptor>,
|
||||
pluginName: PluginName): Map<PluginName, PluginDescriptor>? =
|
||||
if (concretedPlugins[pluginName] != null) {
|
||||
emptyMap()
|
||||
}
|
||||
else {
|
||||
resolvedOnes[pluginName]?.let { plugin ->
|
||||
HashMap<PluginName, PluginDescriptor>().let { res ->
|
||||
res[pluginName] = plugin
|
||||
plugin.deps.keys.forEach { dep ->
|
||||
val subtree = walkSubtree(resolvedOnes, dep)
|
||||
if (subtree == null) {
|
||||
log.warn { "Filtering out plugin ${pluginName.name}, dependency ${dep.name} not found in resolved plugins" }
|
||||
return null
|
||||
}
|
||||
else {
|
||||
res.putAll(subtree)
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
}
|
||||
return hashMapOf<PluginName, PluginDescriptor>().apply {
|
||||
requestedPlugins.mapNotNull { walkSubtree(resolvedOnes, it) }.forEach { m ->
|
||||
putAll(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ResolvedPluginsConfig(val bundlesToLoad: Set<PluginDescriptor>,
|
||||
val shipVersion: PluginVersion) {
|
||||
companion object {
|
||||
val json = Json { prettyPrint = true }
|
||||
}
|
||||
|
||||
override fun toString(): String = json.encodeToString(serializer(), this)
|
||||
}
|
||||
|
||||
data class PluginFetchingResult(
|
||||
val descriptors: Map<PluginName, PluginDescriptor>,
|
||||
val problems: List<PluginResolutionResult.Problem.FetchIssue>
|
||||
)
|
||||
|
||||
private suspend fun PluginRepository.getPlugins(plugins: Map<PluginName, PluginVersion>): PluginFetchingResult =
|
||||
coroutineScope {
|
||||
val problems = mutableListOf<PluginResolutionResult.Problem.FetchIssue>()
|
||||
val descriptors = plugins.map { (name, version) ->
|
||||
name to async {
|
||||
catching { getPlugin(name, version) }
|
||||
}
|
||||
}.mapNotNull { (name, deferred) ->
|
||||
deferred.await().let { result ->
|
||||
result.onFailure { e ->
|
||||
problems.add(PluginResolutionResult.Problem.FetchIssue(name, e))
|
||||
}.getOrNull()?.let {
|
||||
plugin -> name to plugin
|
||||
}
|
||||
}
|
||||
}.toMap()
|
||||
PluginFetchingResult(descriptors, problems)
|
||||
}
|
||||
|
||||
private fun PluginVersion.satisfies(requirement: VersionRequirement): Boolean {
|
||||
return when (requirement) {
|
||||
is VersionRequirement.Above -> this >= requirement.version
|
||||
is VersionRequirement.CompatibleWith -> major == requirement.version.major && this >= requirement.version
|
||||
}
|
||||
}
|
||||
|
||||
private fun PluginResolutionResult.Problem.presentable() = when (this) {
|
||||
is PluginResolutionResult.Problem.Conflict -> "$pluginName - CONFLICT: ${dependencyName.name} requires ${requiredVersion.version}, but ${takenVersion.presentableText} was taken"
|
||||
is PluginResolutionResult.Problem.FetchIssue -> "$pluginName - FETCH ISSUE"
|
||||
is PluginResolutionResult.Problem.PluginNotFound -> "${name.name} - PLUGIN NOT FOUND"
|
||||
}
|
||||
|
||||
fun PluginResolutionResult.simplifiedPresentation(): String = buildString {
|
||||
appendLine()
|
||||
append("PROBLEMS:")
|
||||
append(problems.joinToString(separator = "\n") { it.presentable() })
|
||||
appendLine()
|
||||
append("SHIP version: ${config.shipVersion.presentableText}")
|
||||
appendLine()
|
||||
append("DESCRIPTORS:")
|
||||
appendLine()
|
||||
append(config.bundlesToLoad.joinToString(separator = "\n") { "${it.name.name} - ${it.version.presentableText}" })
|
||||
appendLine()
|
||||
append("_".repeat(40))
|
||||
appendLine()
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package fleet.bundles
|
||||
|
||||
import fleet.util.Base64WithOptionalPadding
|
||||
import fleet.bifurcan.toSortedMap
|
||||
import fleet.bifurcan.toSortedSet
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
internal class VersionSerializer : StringSerializer<VersionRequirement>(
|
||||
toString = {
|
||||
when (it) {
|
||||
is VersionRequirement.CompatibleWith -> it.version.versionString
|
||||
is VersionRequirement.Above -> it.version.versionString + "+"
|
||||
}
|
||||
},
|
||||
fromString = {
|
||||
when {
|
||||
it.endsWith("+") -> VersionRequirement.Above(PluginVersion.fromString(it.dropLast(1)))
|
||||
else -> VersionRequirement.CompatibleWith(PluginVersion.fromString(it))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
internal class PluginNameSerializer : StringSerializer<PluginName>(PluginName::name, ::PluginName)
|
||||
|
||||
internal class PluginVersionSerializer : StringSerializer<PluginVersion>({ it.versionString }, { PluginVersion.fromString(it) })
|
||||
|
||||
/**
|
||||
* Serialize a [PluginVersion] using the string representation compatible with Marketplace compatibility range fields of the plugin descriptor's JSON
|
||||
*/
|
||||
// TODO: remove this and its usages once Marketplace supports Nightly two digit versions in Fleet's compatibility ranges as well
|
||||
internal class PluginVersionForCompatibilityRangeSerializer : StringSerializer<PluginVersion>({ it.marketplaceCompatibilityRangeVersionString }, { PluginVersion.fromString(it) })
|
||||
|
||||
internal class LayerSelectorSerializer : StringSerializer<LayerSelector>(LayerSelector::selector, ::LayerSelector)
|
||||
|
||||
internal object PluginLayerSerializer : DataSerializer<PluginLayer, PluginLayerSurrogate>(
|
||||
serializer = PluginLayerSurrogate.serializer(),
|
||||
toData = PluginLayerSurrogate::fromPluginLayer,
|
||||
fromData = PluginLayerSurrogate::toPluginLayer,
|
||||
)
|
||||
|
||||
/**
|
||||
* Surrogate of [PluginLayer] ensuring sorted [Set] at construction to allow stability at serialization
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("PluginLayer")
|
||||
data class PluginLayerSurrogate(
|
||||
private val modulePath: Set<ModuleCoordinates>,
|
||||
private val modules: Set<String>,
|
||||
private val resources: Set<Coordinates> = emptySet(),
|
||||
) {
|
||||
fun toPluginLayer(): PluginLayer = PluginLayer(
|
||||
modulePath = modulePath,
|
||||
modules = modules,
|
||||
resources = resources,
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun fromPluginLayer(pl: PluginLayer): PluginLayerSurrogate = PluginLayerSurrogate(
|
||||
modulePath = pl.modulePath.toSortedSet(compareBy(coordinatesComparator) { it.coordinates }),
|
||||
modules = pl.modules.toSortedSet(),
|
||||
resources = pl.resources.toSortedSet(coordinatesComparator)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val coordinatesComparator = compareBy<Coordinates> {
|
||||
when (it) {
|
||||
is Coordinates.Local -> it.path
|
||||
is Coordinates.Remote -> it.url
|
||||
}
|
||||
}
|
||||
|
||||
internal object PluginPartsSerializer : DataSerializer<PluginParts, PluginPartsSurrogate>(
|
||||
serializer = PluginPartsSurrogate.serializer(),
|
||||
toData = PluginPartsSurrogate::fromPluginParts,
|
||||
fromData = PluginPartsSurrogate::toPluginParts,
|
||||
)
|
||||
|
||||
/**
|
||||
* Surrogate of [PluginParts] ensuring sorted [Map] at construction to allow stability at serialization
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("PluginDescriptor")
|
||||
internal data class PluginPartsSurrogate(
|
||||
val layers: Map<LayerSelector, PluginLayer>,
|
||||
) {
|
||||
fun toPluginParts(): PluginParts = PluginParts(
|
||||
layers = layers
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun fromPluginParts(pp: PluginParts): PluginPartsSurrogate = PluginPartsSurrogate(
|
||||
layers = pp.layers.toSortedMap(compareBy { selector -> selector.selector })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal object PluginDescriptorSerializer : DataSerializer<PluginDescriptor, PluginDescriptorSurrogate>(
|
||||
serializer = PluginDescriptorSurrogate.serializer(),
|
||||
toData = PluginDescriptorSurrogate::fromPluginDescriptor,
|
||||
fromData = PluginDescriptorSurrogate::toPluginDescriptor,
|
||||
)
|
||||
|
||||
/**
|
||||
* Surrogate of [PluginDescriptor] ensuring sorted [Map] and [Set] at construction to allow stability at serialization
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("PluginDescriptor")
|
||||
internal data class PluginDescriptorSurrogate(
|
||||
private val formatVersion: Int = 0,
|
||||
@SerialName("id")
|
||||
private val name: PluginName,
|
||||
private val version: PluginVersion,
|
||||
@SerialName("dependencies")
|
||||
private val deps: Map<PluginName, VersionRequirement> = emptyMap(),
|
||||
private val compatibleShipVersionRange: ShipVersionRange? = null,
|
||||
private val signature: PluginSignature? = null,
|
||||
private val meta: Map<String, String> = emptyMap(),
|
||||
) {
|
||||
fun toPluginDescriptor(): PluginDescriptor = PluginDescriptor(
|
||||
formatVersion = formatVersion,
|
||||
name = name,
|
||||
version = version,
|
||||
deps = deps,
|
||||
compatibleShipVersionRange = compatibleShipVersionRange,
|
||||
signature = signature,
|
||||
meta = meta,
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun fromPluginDescriptor(pd: PluginDescriptor): PluginDescriptorSurrogate = PluginDescriptorSurrogate(
|
||||
formatVersion = pd.formatVersion,
|
||||
name = pd.name,
|
||||
version = pd.version,
|
||||
deps = pd.deps.toSortedMap(compareBy { pluginName -> pluginName.name }),
|
||||
compatibleShipVersionRange = pd.compatibleShipVersionRange,
|
||||
signature = pd.signature,
|
||||
meta = pd.meta.toSortedMap(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal object PluginSetSerializer : DataSerializer<PluginSet, PluginSetSurrogate>(
|
||||
serializer = PluginSetSurrogate.serializer(),
|
||||
toData = PluginSetSurrogate::fromPluginSet,
|
||||
fromData = PluginSetSurrogate::toPluginSet,
|
||||
)
|
||||
|
||||
/**
|
||||
* Suroggate of [PluginSet] ensuring sorted [Set] at construction to allow stability at serialization
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("PluginSet")
|
||||
internal class PluginSetSurrogate(
|
||||
private val shipVersions: Set<String>,
|
||||
private val plugins: Set<PluginDescriptor>,
|
||||
) {
|
||||
fun toPluginSet(): PluginSet = PluginSet(
|
||||
shipVersions = shipVersions,
|
||||
plugins = plugins,
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun fromPluginSet(ps: PluginSet): PluginSetSurrogate = PluginSetSurrogate(
|
||||
shipVersions = ps.shipVersions.toSortedSet(),
|
||||
plugins = ps.plugins.toSortedSet(compareBy(PluginDescriptor::encodeToString))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
internal object PluginSignatureSerializer : DataSerializer<PluginSignature, String>(
|
||||
serializer = String.serializer(),
|
||||
toData = { value -> Base64WithOptionalPadding.encode(value.bytes) },
|
||||
fromData = { data -> PluginSignature(Base64WithOptionalPadding.decode(data)) }
|
||||
)
|
||||
|
||||
open class StringSerializer<T>(
|
||||
toString: (T) -> String,
|
||||
fromString: (String) -> T,
|
||||
) : DataSerializer<T, String>(String.serializer(), toString, fromString)
|
||||
|
||||
open class DataSerializer<T, D>(
|
||||
val serializer: KSerializer<D>,
|
||||
val toData: (T) -> D,
|
||||
val fromData: (D) -> T,
|
||||
) : KSerializer<T> {
|
||||
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = serializer.descriptor
|
||||
|
||||
override fun deserialize(decoder: Decoder): T {
|
||||
return fromData(serializer.deserialize(decoder))
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: T) {
|
||||
serializer.serialize(encoder, toData(value))
|
||||
}
|
||||
}
|
||||
|
||||
class KnownMeta {
|
||||
companion object {
|
||||
const val ReadableName: String = "readableName"
|
||||
const val Description: String = "description"
|
||||
const val DocumentationUrl: String = "documentation-url"
|
||||
|
||||
/**
|
||||
* Equivalent to vendor `publicName` in Marketplace
|
||||
*/
|
||||
const val VendorPublicName: String = "vendor"
|
||||
|
||||
/**
|
||||
* Equivalent to vendor `name` in Marketplace
|
||||
*/
|
||||
const val VendorId: String = "vendorName"
|
||||
const val Visible: String = "visible"
|
||||
|
||||
const val FrontendOnly: String = "frontend-only"
|
||||
|
||||
const val PartsCoordinates: String = "partsCoordinates"
|
||||
|
||||
const val DefaultIconCoordinates: String = "defaultIconCoordinates"
|
||||
const val DarkIconCoordinates: String = "darkIconCoordinates"
|
||||
|
||||
/**
|
||||
* A set of Fleet product codes with which this plugin is compatible, represented as a comma-separated string
|
||||
*
|
||||
* Used by Marketplace as a discriminant for version parsing.
|
||||
* Format between FL and other Fleet products is no the same, FL uses SemVer, other Fleet products uses IntelliJ versioning.
|
||||
*/
|
||||
const val SupportedProducts: String = "supportedProducts"
|
||||
}
|
||||
}
|
||||
|
||||
internal class JsonFormat {
|
||||
companion object {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
}
|
||||
}
|
||||
|
||||
fun PluginDescriptor.Companion.decodeFromString(s: String): PluginDescriptor = JsonFormat.json.decodeFromString(serializer(), s)
|
||||
|
||||
fun PluginDescriptor.encodeToString(): String = JsonFormat.json.encodeToString(PluginDescriptor.serializer(), this)
|
||||
fun PluginDescriptor.encodeToSignableString(): String = copy(signature = null, compatibleShipVersionRange = null).encodeToString()
|
||||
@@ -0,0 +1,53 @@
|
||||
package fleet.bundles
|
||||
|
||||
private fun PluginLayer.intersection(other: PluginLayer?): PluginLayer? =
|
||||
when {
|
||||
other == null -> null
|
||||
else -> PluginLayer(modulePath = modulePath.intersect(other.modulePath),
|
||||
modules = modules.intersect(other.modules),
|
||||
resources = resources.intersect(other.resources))
|
||||
}
|
||||
|
||||
private fun PluginLayer.merge(other: PluginLayer?): PluginLayer =
|
||||
other?.let {
|
||||
PluginLayer(modulePath = modulePath.union(other.modulePath),
|
||||
modules = modules.union(other.modules),
|
||||
resources = resources.union(other.resources))
|
||||
} ?: this
|
||||
|
||||
|
||||
private fun PluginLayer.subtract(other: PluginLayer?): PluginLayer =
|
||||
when {
|
||||
other == null -> this
|
||||
else -> PluginLayer(modulePath = modulePath.subtract(other.modulePath),
|
||||
modules = modules.subtract(other.modules),
|
||||
resources = resources.subtract(other.resources))
|
||||
}
|
||||
|
||||
fun PluginParts.eliminateIntersections(): PluginParts =
|
||||
eliminateIntersectionsImpl(frontendLayer,
|
||||
workspaceLayer,
|
||||
commonLayer)
|
||||
.eliminateIntersectionsImpl(frontendImplLayer,
|
||||
workspaceImplLayer,
|
||||
commonImplLayer)
|
||||
.eliminateIntersectionsImpl(frontendApiLayer,
|
||||
workspaceApiLayer,
|
||||
commonApiLayer)
|
||||
|
||||
private fun PluginParts.eliminateIntersectionsImpl(frontendS: LayerSelector,
|
||||
workspaceS: LayerSelector,
|
||||
commonS: LayerSelector): PluginParts {
|
||||
val common = layers[frontendS]?.intersection(layers[workspaceS])
|
||||
val pairs = listOf(commonS to (layers[commonS]?.merge(common) ?: common),
|
||||
frontendS to layers[frontendS]?.subtract(common),
|
||||
workspaceS to layers[workspaceS]?.subtract(common))
|
||||
return copy(layers = layers.toMutableMap().apply {
|
||||
pairs.forEach { (selector, layer) ->
|
||||
when {
|
||||
layer != null -> put(selector, layer)
|
||||
else -> remove(selector)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
package fleet.bundles
|
||||
|
||||
import fleet.util.logging.KLoggers
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Serializable
|
||||
enum class PluginVendor(val isSafeToReport: Boolean) {
|
||||
Platform(true), JetBrains(true), ThirdParty(false)
|
||||
}
|
||||
|
||||
@Serializable(with = PluginNameSerializer::class)
|
||||
data class PluginName(val name: String)
|
||||
|
||||
/**
|
||||
* VersionRequirement is what can present in extension's dependencies
|
||||
*/
|
||||
@Serializable(with = VersionSerializer::class)
|
||||
sealed class VersionRequirement {
|
||||
abstract val version: PluginVersion
|
||||
|
||||
data class CompatibleWith(override val version: PluginVersion) : VersionRequirement()
|
||||
data class Above(override val version: PluginVersion) : VersionRequirement()
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents <a href="http://semver.org">Semantic Version</a>.
|
||||
*/
|
||||
@Serializable(with = PluginVersionSerializer::class)
|
||||
data class PluginVersion(
|
||||
val major: Int,
|
||||
val minor: Int,
|
||||
val patch: Int,
|
||||
val preRelease: String?,
|
||||
) : Comparable<PluginVersion> {
|
||||
|
||||
companion object {
|
||||
fun fromString(s: String): PluginVersion {
|
||||
val major = s.substringBefore('.').toIntOrNull()
|
||||
return when {
|
||||
major == null || major < 0 -> throw IllegalArgumentException("Cannot parse `$s` as PluginVersion, must start by a strictly positive integer")
|
||||
major <= 1 -> fromSemverString(s)
|
||||
else -> fromIntelliJUnifiedVersioningString(s)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy build numbering of FL was using SemVer
|
||||
*/
|
||||
private fun fromSemverString(s: String): PluginVersion {
|
||||
val majorEndIdx = s.indexOf('.')
|
||||
if (majorEndIdx >= 0) {
|
||||
val minorEndIdx = s.indexOf('.', majorEndIdx + 1)
|
||||
if (minorEndIdx >= 0) {
|
||||
val preReleaseIdx = s.indexOf('-', minorEndIdx + 1)
|
||||
val patchEndIdx = if (preReleaseIdx >= 0) preReleaseIdx else s.length
|
||||
val major = s.substring(0, majorEndIdx).toIntOrNull()
|
||||
val minor = s.substring(majorEndIdx + 1, minorEndIdx).toIntOrNull()
|
||||
val patch = s.substring(minorEndIdx + 1, patchEndIdx).toIntOrNull()
|
||||
val preRelease = if (preReleaseIdx >= 0) s.substring(preReleaseIdx + 1) else null
|
||||
if (major != null && minor != null && patch != null) {
|
||||
return PluginVersion(major = major, minor = minor, patch = patch, preRelease = preRelease)
|
||||
}
|
||||
}
|
||||
}
|
||||
throw IllegalArgumentException("Cannot parse `$s` as PluginVersion.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Build numbering of AIR and next products of the Fleet platform, using https://youtrack.jetbrains.com/articles/IJPL-A-109
|
||||
*/
|
||||
private fun fromIntelliJUnifiedVersioningString(s: String): PluginVersion {
|
||||
val intComponents = s.split(".", limit = 3).map {
|
||||
it.toIntOrSnapshotOrNull() ?: throw IllegalArgumentException("Cannot parse `$s` as PluginVersion, all components must be integers")
|
||||
}
|
||||
return when (intComponents.size) {
|
||||
3 -> {
|
||||
val (major, minor, patch) = intComponents
|
||||
when (major) {
|
||||
UnifiedVersionComponent.Snapshot -> throw IllegalArgumentException("Cannot parse `$s` as PluginVersion, major cannot be '$SNAPSHOT'")
|
||||
is UnifiedVersionComponent.IntComponent -> when (minor) {
|
||||
UnifiedVersionComponent.Snapshot -> throw IllegalArgumentException("Cannot parse `$s` as PluginVersion, minor cannot be '$SNAPSHOT' if there is a patch number")
|
||||
is UnifiedVersionComponent.IntComponent -> when (patch) {
|
||||
is UnifiedVersionComponent.IntComponent -> PluginVersion(major = major.value, minor = minor.value, patch = patch.value, preRelease = null)
|
||||
UnifiedVersionComponent.Snapshot -> PluginVersion(major = major.value, minor = minor.value, patch = 0, preRelease = SNAPSHOT) // hackily set to `0`, I don't see what else we could do here
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2 -> { // nightly build
|
||||
val (major, minor) = intComponents
|
||||
when (major) {
|
||||
UnifiedVersionComponent.Snapshot -> throw IllegalArgumentException("Cannot parse `$s` as PluginVersion, major cannot be '$SNAPSHOT'")
|
||||
is UnifiedVersionComponent.IntComponent -> when (minor) {
|
||||
is UnifiedVersionComponent.IntComponent -> PluginVersion(major = major.value, minor = minor.value, patch = 0, preRelease = null) // hackily set to `0`, I don't see what else we could do here
|
||||
UnifiedVersionComponent.Snapshot -> PluginVersion(major = major.value, minor = 0, patch = 0, preRelease = SNAPSHOT) // hackily set to `0`, I don't see what else we could do here
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> throw IllegalArgumentException("Cannot parse `$s` as PluginVersion, must be either XXX.YYY or XXX.YYY.ZZZ")
|
||||
}
|
||||
}
|
||||
|
||||
private const val SNAPSHOT = "SNAPSHOT"
|
||||
|
||||
sealed class UnifiedVersionComponent {
|
||||
data class IntComponent(val value: Int) : UnifiedVersionComponent()
|
||||
data object Snapshot : UnifiedVersionComponent()
|
||||
}
|
||||
|
||||
fun String.toIntOrSnapshotOrNull(): UnifiedVersionComponent? = when (this) {
|
||||
SNAPSHOT -> UnifiedVersionComponent.Snapshot
|
||||
else -> toIntOrNull()?.let { UnifiedVersionComponent.IntComponent(it) }
|
||||
}
|
||||
}
|
||||
|
||||
val presentableText: String get() = versionString
|
||||
|
||||
/**
|
||||
* String representation of this [PluginVersion] for Marketplace compatibility range fields of the plugin descriptor's JSON
|
||||
*/
|
||||
val marketplaceCompatibilityRangeVersionString: String
|
||||
get() = buildVersionString(withSemverPatchEvenWhenZero = true)
|
||||
|
||||
val versionString: String
|
||||
get() = buildVersionString(withSemverPatchEvenWhenZero = false)
|
||||
|
||||
/**
|
||||
* Build the [String] representation of that [PluginVersion] used in business logic.
|
||||
*
|
||||
* Handling of the case where patch=0 depends on [withSemverPatchEvenWhenZero] parameter.
|
||||
*
|
||||
* Note: patch=0 usually happens when the original plugin version was a Nightly version.
|
||||
* Indeed, Nightly versions do not contain a patch component.
|
||||
* For backward compatibility, in such a case, we had to set it to 0 until we change [PluginVersion] altogether not to represent a SemVer.
|
||||
*
|
||||
* @param withSemverPatchEvenWhenZero whether to include a SemVer compatible `.0` patch when patch=0
|
||||
*/
|
||||
private fun buildVersionString(withSemverPatchEvenWhenZero: Boolean): String = buildString {
|
||||
append(major)
|
||||
append(".")
|
||||
append(minor)
|
||||
if (withSemverPatchEvenWhenZero || patch != 0) {
|
||||
append(".")
|
||||
append(patch)
|
||||
}
|
||||
if (preRelease != null) {
|
||||
append("-$preRelease")
|
||||
}
|
||||
}
|
||||
|
||||
override fun compareTo(other: PluginVersion): Int {
|
||||
val result = compareValuesBy(this, other, { it.major }, { it.minor }, { it.patch })
|
||||
return if (result != 0) {
|
||||
result
|
||||
}
|
||||
else {
|
||||
comparePreRelease(preRelease, other.preRelease)
|
||||
}
|
||||
}
|
||||
|
||||
private fun comparePreRelease(pre1: String?, pre2: String?): Int {
|
||||
return when {
|
||||
pre1 == pre2 -> 0
|
||||
pre1 == null -> 1
|
||||
pre2 == null -> -1
|
||||
else -> {
|
||||
val iterator1 = pre1.splitToSequence('.').iterator()
|
||||
val iterator2 = pre2.splitToSequence('.').iterator()
|
||||
|
||||
while (iterator1.hasNext() && iterator2.hasNext()) {
|
||||
val segment1 = iterator1.next()
|
||||
val intSegment1 = segment1.toIntOrNull()
|
||||
val segment2 = iterator2.next()
|
||||
val intSegment2 = segment2.toIntOrNull()
|
||||
val result = when {
|
||||
intSegment1 != null && intSegment2 != null -> {
|
||||
intSegment1.compareTo(intSegment2)
|
||||
}
|
||||
intSegment1 == null && intSegment2 == null -> {
|
||||
segment1.compareTo(segment2)
|
||||
}
|
||||
intSegment1 == null -> {
|
||||
// According to SemVer specification numeric segments has lower precedence
|
||||
// than non-numeric segments
|
||||
1
|
||||
}
|
||||
else -> -1
|
||||
}
|
||||
if (result != 0) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return if (iterator1.hasNext()) {
|
||||
1
|
||||
}
|
||||
else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// should be in sync with https://github.com/JetBrains/intellij-plugin-verifier/blob/6a04cd7c94eb806877e26a093378eaf2b85e0d73/intellij-plugin-structure/structure-fleet/src/main/kotlin/com/jetbrains/plugin/structure/fleet/FleetPluginDescriptor.kt#L146
|
||||
fun toLong(): Long {
|
||||
val components = buildList {
|
||||
if (major != 0) {
|
||||
add(major)
|
||||
}
|
||||
if (minor != 0) {
|
||||
add(minor)
|
||||
}
|
||||
if (patch != 0) {
|
||||
add(patch)
|
||||
}
|
||||
if (preRelease != null) {
|
||||
add(SNAPSHOT_VALUE)
|
||||
}
|
||||
}
|
||||
return CompatibilityUtils.versionAsLong(components.toIntArray())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a [PluginDescriptor]'s value
|
||||
*/
|
||||
@Serializable(with = PluginDescriptorSerializer::class)
|
||||
data class PluginDescriptor(
|
||||
val formatVersion: Int = 0,
|
||||
val name: PluginName,
|
||||
val version: PluginVersion,
|
||||
val deps: Map<PluginName, VersionRequirement> = emptyMap(),
|
||||
val compatibleShipVersionRange: ShipVersionRange? = null,
|
||||
val signature: PluginSignature? = null,
|
||||
val meta: Map<String, String> = emptyMap(),
|
||||
) {
|
||||
override fun toString(): String = prettyJson.encodeToString(serializer(), this)
|
||||
}
|
||||
|
||||
private val prettyJson = Json {
|
||||
prettyPrint = true
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
private val defaultJson = Json { // we cannot depend on `fleet.util.serialization.DefaultJson` from `fleet.bundles`
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
explicitNulls = false
|
||||
}
|
||||
|
||||
val PluginDescriptor.partsCoordinates: Coordinates? get() = metaAsCoordinates(KnownMeta.PartsCoordinates)
|
||||
val PluginDescriptor.defaultIcon: Coordinates? get() = metaAsCoordinates(KnownMeta.DefaultIconCoordinates)
|
||||
val PluginDescriptor.darkIcon: Coordinates? get() = metaAsCoordinates(KnownMeta.DarkIconCoordinates)
|
||||
|
||||
fun PluginDescriptor.metaAsCoordinates(metaKey: String): Coordinates? = meta[metaKey]?.let { serializedCoordinates ->
|
||||
defaultJson.decodeFromString(Coordinates.serializer(), serializedCoordinates)
|
||||
}
|
||||
|
||||
private const val JETBRAINS_VENDOR = "JetBrains"
|
||||
|
||||
fun PluginDescriptor?.getVendorType(): PluginVendor {
|
||||
val vendorId = this?.meta?.get(KnownMeta.VendorId)
|
||||
|
||||
return when {
|
||||
vendorId == null -> PluginVendor.Platform
|
||||
vendorId == JETBRAINS_VENDOR -> PluginVendor.JetBrains
|
||||
else -> {
|
||||
PluginVendor.ThirdParty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable(with = PluginSignatureSerializer::class)
|
||||
data class PluginSignature(val bytes: ByteArray) {
|
||||
override fun equals(other: Any?): Boolean =
|
||||
other is PluginSignature && other.bytes.contentEquals(bytes)
|
||||
|
||||
override fun hashCode(): Int =
|
||||
bytes.contentHashCode()
|
||||
|
||||
override fun toString(): String =
|
||||
"PluginSignature(size=${bytes.size}, hash=${hashCode().toString(16)}"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ShipVersionRange(
|
||||
@Serializable(with = PluginVersionForCompatibilityRangeSerializer::class)
|
||||
val from: PluginVersion,
|
||||
@Serializable(with = PluginVersionForCompatibilityRangeSerializer::class)
|
||||
val to: PluginVersion,
|
||||
)
|
||||
|
||||
@Serializable(with = LayerSelectorSerializer::class)
|
||||
data class LayerSelector(val selector: String)
|
||||
|
||||
@Serializable
|
||||
data class ModuleCoordinates(
|
||||
val coordinates: Coordinates,
|
||||
val serializedModuleDescriptor: String?,
|
||||
)
|
||||
|
||||
@Serializable(with = PluginLayerSerializer::class)
|
||||
data class PluginLayer(
|
||||
val modulePath: Set<ModuleCoordinates>,
|
||||
val modules: Set<String>,
|
||||
val resources: Set<Coordinates>,
|
||||
)
|
||||
|
||||
@Serializable(with = PluginPartsSerializer::class)
|
||||
data class PluginParts(val layers: Map<LayerSelector, PluginLayer>)
|
||||
|
||||
@Serializable
|
||||
sealed interface Coordinates {
|
||||
val meta: Map<String, String>
|
||||
|
||||
// to reference e.g. a plugin file in marketplace (which might also be in code-cache already)
|
||||
@Serializable
|
||||
@SerialName("Remote")
|
||||
data class Remote(val url: String, val hash: String, override val meta: Map<String, String> = emptyMap()) : Coordinates {
|
||||
companion object {
|
||||
const val HASH_ALGORITHM: String = "SHA3-256"
|
||||
}
|
||||
}
|
||||
|
||||
// to reference a folder with classes, should be used when running from sources only
|
||||
@Serializable
|
||||
@SerialName("Local")
|
||||
data class Local(val path: String, override val meta: Map<String, String> = emptyMap()) : Coordinates
|
||||
}
|
||||
|
||||
class KnownCoordinatesMeta {
|
||||
companion object {
|
||||
const val Platforms: String = "platforms"
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable(with = PluginSetSerializer::class)
|
||||
data class PluginSet(
|
||||
val shipVersions: Set<String>,
|
||||
val plugins: Set<PluginDescriptor>,
|
||||
)
|
||||
|
||||
private const val SNAPSHOT_VALUE = Int.MAX_VALUE
|
||||
|
||||
private object CompatibilityUtils {
|
||||
private const val MAX_BUILD_VALUE = 100000
|
||||
private const val MAX_COMPONENT_VALUE = 10000
|
||||
private val NUMBERS_OF_NINES by lazy { initNumberOfNines() }
|
||||
|
||||
private fun initNumberOfNines(): IntArray {
|
||||
val numbersOfNines = ArrayList<Int>()
|
||||
var i = 99999
|
||||
val maxIntDiv10 = Int.MAX_VALUE / 10
|
||||
while (i < maxIntDiv10) {
|
||||
i = i * 10 + 9
|
||||
numbersOfNines.add(i)
|
||||
}
|
||||
|
||||
return numbersOfNines.toIntArray()
|
||||
}
|
||||
|
||||
fun versionAsLong(components: IntArray): Long {
|
||||
val baselineVersion = components.getOrElse(0) { 0 }
|
||||
val build = components.getOrElse(1) { 0 }
|
||||
var longVersion = branchBuildAsLong(baselineVersion, build)
|
||||
|
||||
if (components.size >= 3) {
|
||||
val component = components[2]
|
||||
longVersion += if (component == Int.MAX_VALUE) MAX_COMPONENT_VALUE - 1 else component
|
||||
}
|
||||
|
||||
return longVersion
|
||||
}
|
||||
|
||||
private fun isNumberOfNines(p: Int) = NUMBERS_OF_NINES.any { it == p }
|
||||
|
||||
private fun branchBuildAsLong(branch: Int, build: Int): Long {
|
||||
val result = if (build == Int.MAX_VALUE || isNumberOfNines(build)) {
|
||||
MAX_BUILD_VALUE - 1
|
||||
}
|
||||
else {
|
||||
build
|
||||
}
|
||||
|
||||
return branch.toLong() * MAX_COMPONENT_VALUE * MAX_BUILD_VALUE + result.toLong() * MAX_COMPONENT_VALUE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package fleet.bundles
|
||||
|
||||
import fleet.multiplatform.shims.multiplatformIO
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.nio.file.Path
|
||||
import java.util.zip.ZipInputStream
|
||||
import kotlin.io.path.pathString
|
||||
import kotlin.io.path.readBytes
|
||||
import kotlin.io.path.readText
|
||||
|
||||
class CoordinatesResolutionImpl(private val fileResolver: suspend (Coordinates) -> Path) : CoordinatesResolution {
|
||||
companion object {
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun resolveFile(coordinates: Coordinates): ResolvedFile {
|
||||
return withContext(Dispatchers.multiplatformIO) {
|
||||
ResolvedFile(fileResolver(coordinates).pathString)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun resolvePluginPartsCoordinates(coordinates: Coordinates): PluginParts {
|
||||
val parts = withContext(Dispatchers.multiplatformIO) {
|
||||
fileResolver(coordinates).readText()
|
||||
}
|
||||
return json.decodeFromString(PluginParts.serializer(), parts).eliminateIntersections()
|
||||
}
|
||||
|
||||
override suspend fun resolveModule(coordinates: ModuleCoordinates): ModuleOnDisc {
|
||||
return withContext(Dispatchers.multiplatformIO) {
|
||||
ModuleOnDisc(path = fileResolver(coordinates.coordinates).pathString,
|
||||
serializedModuleDescriptor = coordinates.serializedModuleDescriptor)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun resolveResource(coordinates: Coordinates): ResourceBundle {
|
||||
val pathString = resolveFile(coordinates).path
|
||||
val resources = Path.of(pathString)
|
||||
.readBytes()
|
||||
.let(::unzip)
|
||||
return ResourceBundle { resources[it.removePrefix("/")] }
|
||||
}
|
||||
}
|
||||
|
||||
fun unzip(zipBytes: ByteArray): Map<String, ByteArray> {
|
||||
return ZipInputStream(zipBytes.inputStream()).use { zip ->
|
||||
generateSequence { zip.nextEntry }.filter { !it.isDirectory }.associate { it.name.removePrefix("/") to zip.readBytes() }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user