From fd23a03618b4cb21efa674d4565f2458745d254a Mon Sep 17 00:00:00 2001 From: Him188 Date: Sat, 12 Sep 2020 22:34:15 +0800 Subject: [PATCH] Introduce version DSL --- .../MiraiConsoleImplementationBridge.kt | 6 +- .../internal/plugin/PluginManagerImpl.kt | 27 ++- .../plugin/description/PluginDependency.kt | 18 +- .../plugin/description/VersionRequirement.kt | 210 ++++++++++++++++++ .../plugin/jvm/JvmPluginDescription.kt | 76 ++++++- 5 files changed, 303 insertions(+), 34 deletions(-) create mode 100644 backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/VersionRequirement.kt diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/MiraiConsoleImplementationBridge.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/MiraiConsoleImplementationBridge.kt index 2bbad7896..a971e6e3a 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/MiraiConsoleImplementationBridge.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/MiraiConsoleImplementationBridge.kt @@ -60,6 +60,7 @@ import kotlin.coroutines.CoroutineContext /** * [MiraiConsole] 公开 API 与前端实现的连接桥. */ +@Suppress("SpellCheckingInspection") internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleImplementation, MiraiConsole { override val pluginCenter: PluginCenter get() = throw UnsupportedOperationException("PluginCenter is not supported yet") @@ -219,11 +220,12 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI mainLogger.info { "mirai-console started successfully." } } + @Suppress("SpellCheckingInspection") @Retention(AnnotationRetention.SOURCE) @DslMarker - private annotation class MiraiIsCool + private annotation class ILoveOmaeKumikoForever - @MiraiIsCool + @ILoveOmaeKumikoForever private inline fun phase(block: () -> Unit) { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/PluginManagerImpl.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/PluginManagerImpl.kt index f84e8f4c5..f53ebac6f 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/PluginManagerImpl.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/internal/plugin/PluginManagerImpl.kt @@ -187,7 +187,15 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol private fun List.sortByDependencies(): List { val resolved = ArrayList(this.size) - fun D.canBeLoad(): Boolean = this.dependencies.all { it.isOptional || it in resolved } + fun D.canBeLoad(): Boolean = this.dependencies.all { dependency -> + val target = resolved.findDependency(dependency) + if (target == null) { + dependency.isOptional + } else { + target.checkSatisfies(dependency, this@canBeLoad) + true + } + } fun List.consumeLoadable(): List { val (canBeLoad, cannotBeLoad) = this.partition { it.canBeLoad() } @@ -196,7 +204,7 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol } fun Collection.filterIsMissing(): List = - this.filterNot { it.isOptional || it in resolved } + this.filterNot { it.isOptional || resolved.findDependency(it) != null } fun List.doSort() { if (this.isEmpty()) return @@ -206,8 +214,7 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol check(resultPlugins.size < beforeSize) { throw PluginMissingDependencyException(resultPlugins.joinToString("\n") { badPlugin -> "Cannot load plugin ${badPlugin.name}, missing dependencies: ${ - badPlugin.dependencies.filterIsMissing() - .joinToString() + badPlugin.dependencies.filterIsMissing().joinToString() }" }) } @@ -235,5 +242,13 @@ internal fun PluginDescription.wrapWith(loader: PluginLoader<*, *>, plugin: Plug loader as PluginLoader, this, plugin ) -internal operator fun List.contains(dependency: PluginDependency): Boolean = - any { it.id == dependency.id } +internal fun List.findDependency(dependency: PluginDependency): PluginDescription? { + return find { it.id.equals(dependency.id, ignoreCase = true) } +} + +internal fun PluginDescription.checkSatisfies(dependency: PluginDependency, plugin: PluginDescription) { + val requirement = dependency.versionRequirement + if (requirement != null && this.version !in requirement) { + throw PluginLoadException("Plugin '${plugin.id}' ('${plugin.id}') requires '${dependency.id}' with version $requirement while the resolved is ${this.version}") + } +} diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDependency.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDependency.kt index e62ba0482..49bfad6ff 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDependency.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/PluginDependency.kt @@ -11,8 +11,6 @@ package net.mamoe.mirai.console.plugin.description -import com.vdurmont.semver4j.Semver - /** * 插件的一个依赖的信息. * @@ -28,13 +26,14 @@ public data class PluginDependency @JvmOverloads constructor( * * 版本遵循 [语义化版本 2.0 规范](https://semver.org/lang/zh-CN/), * - * 允许 [Apache Ivy 风格版本号表示](http://ant.apache.org/ivy/history/latest-milestone/settings/version-matchers.html) + * ### 示例 + * `Requirement.buildIvy("[1.0, 2.0)")` */ - public val version: Semver? = null, + public val versionRequirement: VersionRequirement? = null, /** * 若为 `false`, 插件在找不到此依赖时也能正常加载. */ - public val isOptional: Boolean = false + public val isOptional: Boolean = false, ) { init { kotlin.runCatching { @@ -50,13 +49,4 @@ public data class PluginDependency @JvmOverloads constructor( public constructor(name: String, isOptional: Boolean = false) : this( name, null, isOptional ) - - /** - * @see PluginDependency - */ - public constructor(name: String, version: String, isOptional: Boolean) : this( - name, - Semver(version, Semver.SemverType.IVY), - isOptional - ) } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/VersionRequirement.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/VersionRequirement.kt new file mode 100644 index 000000000..263e625ea --- /dev/null +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/description/VersionRequirement.kt @@ -0,0 +1,210 @@ +package net.mamoe.mirai.console.plugin.description + +import com.vdurmont.semver4j.Requirement +import com.vdurmont.semver4j.Semver + +public sealed class VersionRequirement { + public abstract operator fun contains(version: Semver): Boolean + public fun contains(version: String): Boolean = contains(Semver(version, Semver.SemverType.LOOSE)) + + public data class Exact( + val version: Semver, + ) : VersionRequirement() { + public constructor(version: String) : this(Semver(version, Semver.SemverType.LOOSE)) + + override fun contains(version: Semver): Boolean = this.version.isEqualTo(version) + } + + public data class MatchesNpmPattern( + val pattern: String, + ) : VersionRequirement() { + private val requirement = Requirement.buildNPM(pattern) + override fun contains(version: Semver): Boolean = requirement.isSatisfiedBy(version) + } + + public data class MatchesIvyPattern( + val pattern: String, + ) : VersionRequirement() { + private val requirement = Requirement.buildIvy(pattern) + override fun contains(version: Semver): Boolean = requirement.isSatisfiedBy(version) + } + + + public data class MatchesCocoapodsPattern( + val pattern: String, + ) : VersionRequirement() { + private val requirement = Requirement.buildCocoapods(pattern) + override fun contains(version: Semver): Boolean = requirement.isSatisfiedBy(version) + } + + public abstract class Custom : VersionRequirement() + + public data class InRange( + val begin: Semver, + val beginInclusive: Boolean, + val end: Semver, + val endInclusive: Boolean, + ) : VersionRequirement() { + public constructor( + begin: String, + beginInclusive: Boolean, + end: Semver, + endInclusive: Boolean, + ) : this(Semver(begin, Semver.SemverType.LOOSE), beginInclusive, end, endInclusive) + + public constructor( + begin: String, + beginInclusive: Boolean, + end: String, + endInclusive: Boolean, + ) : this(Semver(begin, Semver.SemverType.LOOSE), + beginInclusive, + Semver(end, Semver.SemverType.LOOSE), + endInclusive) + + public constructor( + begin: Semver, + beginInclusive: Boolean, + end: String, + endInclusive: Boolean, + ) : this(begin, beginInclusive, Semver(end, Semver.SemverType.LOOSE), endInclusive) + + override fun contains(version: Semver): Boolean { + return if (beginInclusive) { + version.isGreaterThanOrEqualTo(begin) + } else { + version.isGreaterThan(begin) + } && if (endInclusive) { + version.isLowerThanOrEqualTo(begin) + } else { + version.isLowerThan(begin) + } + } + } + + + @Suppress("unused") + public class Builder { + @ILoveKafuuChinoForever + public fun exact(version: Semver): VersionRequirement = Exact(version) + + @ILoveKafuuChinoForever + public fun exact(version: String): VersionRequirement = Exact(version) + + @ILoveKafuuChinoForever + public fun custom(checker: (version: Semver) -> Boolean): VersionRequirement { + return object : Custom() { + override fun contains(version: Semver): Boolean = checker(version) + } + } + + /** + * @see Semver.SemverType.NPM + */ + @ILoveKafuuChinoForever + public fun npmPattern(versionPattern: String): VersionRequirement { + return MatchesNpmPattern(versionPattern) + } + + /** + * @see Semver.SemverType.IVY + */ + @ILoveKafuuChinoForever + public fun ivyPattern(versionPattern: String): VersionRequirement { + return MatchesIvyPattern(versionPattern) + } + + /** + * @see Semver.SemverType.COCOAPODS + */ + @ILoveKafuuChinoForever + public fun cocoapodsPattern(versionPattern: String): VersionRequirement { + return MatchesCocoapodsPattern(versionPattern) + } + + @ILoveKafuuChinoForever + public fun range( + begin: Semver, + beginInclusive: Boolean, + end: Semver, + endInclusive: Boolean, + ): VersionRequirement = InRange(begin, beginInclusive, end, endInclusive) + + @ILoveKafuuChinoForever + public fun range( + begin: String, + beginInclusive: Boolean, + end: Semver, + endInclusive: Boolean, + ): VersionRequirement = InRange(begin, beginInclusive, end, endInclusive) + + @ILoveKafuuChinoForever + public fun range( + begin: Semver, + beginInclusive: Boolean, + end: String, + endInclusive: Boolean, + ): VersionRequirement = InRange(begin, beginInclusive, end, endInclusive) + + @ILoveKafuuChinoForever + public fun range( + begin: String, + beginInclusive: Boolean, + end: String, + endInclusive: Boolean, + ): VersionRequirement = InRange(begin, beginInclusive, end, endInclusive) + + + @ILoveKafuuChinoForever + public operator fun Semver.rangeTo(endInclusive: Semver): VersionRequirement { + return InRange(this, true, endInclusive, true) + } + + @ILoveKafuuChinoForever + public operator fun Semver.rangeTo(endInclusive: String): VersionRequirement { + return InRange(this, true, Semver(endInclusive, Semver.SemverType.LOOSE), true) + } + + @ILoveKafuuChinoForever + public operator fun String.rangeTo(endInclusive: String): VersionRequirement { + return InRange(Semver(this, Semver.SemverType.LOOSE), + true, + Semver(endInclusive, Semver.SemverType.LOOSE), + true) + } + + @ILoveKafuuChinoForever + public operator fun String.rangeTo(endInclusive: Semver): VersionRequirement { + return InRange(Semver(this, Semver.SemverType.LOOSE), true, endInclusive, true) + } + + + @ILoveKafuuChinoForever + public infix fun Semver.until(endExclusive: Semver): VersionRequirement { + return InRange(this, true, endExclusive, false) + } + + @ILoveKafuuChinoForever + public infix fun Semver.until(endExclusive: String): VersionRequirement { + return InRange(this, true, Semver(endExclusive, Semver.SemverType.LOOSE), false) + } + + @ILoveKafuuChinoForever + public infix fun String.until(endExclusive: String): VersionRequirement { + return InRange(Semver(this, Semver.SemverType.LOOSE), + true, + Semver(endExclusive, Semver.SemverType.LOOSE), + false) + } + + @ILoveKafuuChinoForever + public infix fun String.until(endExclusive: Semver): VersionRequirement { + return InRange(Semver(this, Semver.SemverType.LOOSE), true, endExclusive, false) + } + + @Suppress("SpellCheckingInspection") + @Retention(AnnotationRetention.SOURCE) + @DslMarker + private annotation class ILoveKafuuChinoForever + } +} \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginDescription.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginDescription.kt index 7848826aa..cb4807447 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginDescription.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugin/jvm/JvmPluginDescription.kt @@ -14,6 +14,7 @@ package net.mamoe.mirai.console.plugin.jvm import com.vdurmont.semver4j.Semver import net.mamoe.mirai.console.plugin.description.PluginDependency import net.mamoe.mirai.console.plugin.description.PluginDescription +import net.mamoe.mirai.console.plugin.description.VersionRequirement /** * JVM 插件的描述. 通常作为 `plugin.yml` @@ -122,16 +123,6 @@ public class JvmPluginDescriptionBuilder( @ILoveKuriyamaMiraiForever public fun info(value: String): JvmPluginDescriptionBuilder = apply { this.info = value.trimIndent() } - @ILoveKuriyamaMiraiForever - public fun dependsOn( - pluginId: String, - version: String? = null, - isOptional: Boolean = false, - ): JvmPluginDescriptionBuilder = apply { - if (version == null) this.dependencies.add(PluginDependency(pluginId, version, isOptional)) - else this.dependencies.add(PluginDependency(pluginId, version, isOptional)) - } - @ILoveKuriyamaMiraiForever public fun setDependencies( value: Set, @@ -148,18 +139,79 @@ public class JvmPluginDescriptionBuilder( } } + /** + * @see PluginDependency + */ @ILoveKuriyamaMiraiForever public fun dependsOn( pluginId: String, - version: Semver? = null, isOptional: Boolean = false, - ): JvmPluginDescriptionBuilder = apply { this.dependencies.add(PluginDependency(pluginId, version, isOptional)) } + versionRequirement: VersionRequirement? = null, + ): JvmPluginDescriptionBuilder = apply { + if (versionRequirement == null) + this.dependencies.add(PluginDependency(pluginId, versionRequirement, isOptional)) + else this.dependencies.add(PluginDependency(pluginId, versionRequirement, isOptional)) + } + + /** + * isOptional = false + * + * @see PluginDependency + */ + @ILoveKuriyamaMiraiForever + public fun dependsOn( + pluginId: String, + versionRequirement: VersionRequirement? = null, + ): JvmPluginDescriptionBuilder = apply { + if (versionRequirement == null) + this.dependencies.add(PluginDependency(pluginId, versionRequirement, false)) + else this.dependencies.add(PluginDependency(pluginId, versionRequirement, false)) + } + + /** + * 无版本要求 + * + * @see PluginDependency + */ + @ILoveKuriyamaMiraiForever + public fun dependsOn( + pluginId: String, + isOptional: Boolean = false, + ): JvmPluginDescriptionBuilder = apply { + dependsOn(pluginId, isOptional, null) + } + + /** + * 示例: + * + * ``` + * dependsOn("org.example.test-plugin") { "1.0.0".."1.2.0" } + * dependsOn("org.example.test-plugin") { npmPattern("1.x || >=2.5.0 || 5.0.0 - 7.2.3") } + * dependsOn("org.example.test-plugin") { ivyPattern("[1.0,2.0[") } + * dependsOn("org.example.test-plugin") { custom { it.toString() == "1.0.0" } } + * ``` + * + * @see PluginDependency + * @see VersionRequirement.Builder + */ + @ILoveKuriyamaMiraiForever + public fun dependsOn( + pluginId: String, + isOptional: Boolean = false, + versionRequirement: VersionRequirement.Builder.() -> VersionRequirement, + ): JvmPluginDescriptionBuilder = + apply { + this.dependencies.add(PluginDependency(pluginId, + VersionRequirement.Builder().run(versionRequirement), + isOptional)) + } @Suppress("DEPRECATION_ERROR") public fun build(): JvmPluginDescription = SimpleJvmPluginDescription(name, version, id, author, info, dependencies) + @Suppress("SpellCheckingInspection") @Retention(AnnotationRetention.SOURCE) @DslMarker private annotation class ILoveKuriyamaMiraiForever // https://zh.moegirl.org.cn/zh-cn/%E6%A0%97%E5%B1%B1%E6%9C%AA%E6%9D%A5