diff --git a/platform/core-api/api-dump-experimental.txt b/platform/core-api/api-dump-experimental.txt index 2a44d092511a..bf5c2d09b2fc 100644 --- a/platform/core-api/api-dump-experimental.txt +++ b/platform/core-api/api-dump-experimental.txt @@ -55,6 +55,7 @@ com.intellij.ide.plugins.DynamicPluginListener - s:values():com.intellij.ide.plugins.IdeaPluginOsRequirement[] *f:com.intellij.ide.plugins.IdeaPluginOsRequirement$Companion - f:fromModuleId(com.intellij.openapi.extensions.PluginId):com.intellij.ide.plugins.IdeaPluginOsRequirement +- f:fromOs(com.intellij.util.system.OS):com.intellij.ide.plugins.IdeaPluginOsRequirement - f:getHostOsModuleIds():java.util.List *a:com.intellij.lang.LanguageMatcher - s:match(com.intellij.lang.Language):com.intellij.lang.LanguageMatcher diff --git a/platform/core-api/src/com/intellij/ide/plugins/IdeaPluginOsRequirement.kt b/platform/core-api/src/com/intellij/ide/plugins/IdeaPluginOsRequirement.kt index 7726b4e3f50c..e6848d6db039 100644 --- a/platform/core-api/src/com/intellij/ide/plugins/IdeaPluginOsRequirement.kt +++ b/platform/core-api/src/com/intellij/ide/plugins/IdeaPluginOsRequirement.kt @@ -48,6 +48,14 @@ enum class IdeaPluginOsRequirement { fun fromModuleId(moduleId: PluginId): IdeaPluginOsRequirement? = directory[moduleId] ?: Unknown.takeIf { looksLikeOsModuleId(moduleId.idString) } + fun fromOs(os: OS): IdeaPluginOsRequirement? = when (os) { + OS.Windows -> Windows + OS.macOS -> Mac + OS.Linux -> Linux + OS.FreeBSD -> FreeBSD + OS.Other -> null + } + private fun looksLikeOsModuleId(idString: String): Boolean = idString.startsWith(osModuleIdPrefix) } } diff --git a/platform/core-api/src/com/intellij/ide/plugins/PluginCpuArchRequirement.kt b/platform/core-api/src/com/intellij/ide/plugins/PluginCpuArchRequirement.kt index 852fde5686e0..c488c81741a5 100644 --- a/platform/core-api/src/com/intellij/ide/plugins/PluginCpuArchRequirement.kt +++ b/platform/core-api/src/com/intellij/ide/plugins/PluginCpuArchRequirement.kt @@ -44,6 +44,10 @@ enum class PluginCpuArchRequirement { return directory[pluginId] ?: Unknown.takeIf { looksLikeCpuArchModuleId(pluginId.idString) } } + fun fromArch(arch: CpuArch): PluginCpuArchRequirement? { + return fromPluginId(PluginId.getId(archPluginAliasIdPrefix + arch.name.lowercase())) + } + private fun looksLikeCpuArchModuleId(idString: String): Boolean = idString.startsWith(archPluginAliasIdPrefix) } } diff --git a/platform/core-impl/src/com/intellij/ide/plugins/PluginManagerCore.kt b/platform/core-impl/src/com/intellij/ide/plugins/PluginManagerCore.kt index 153735ae02d4..168159f4174c 100644 --- a/platform/core-impl/src/com/intellij/ide/plugins/PluginManagerCore.kt +++ b/platform/core-impl/src/com/intellij/ide/plugins/PluginManagerCore.kt @@ -48,6 +48,8 @@ private const val PLATFORM_ALIAS_DEPENDENCY_PREFIX = "com.intellij.module" internal val QODANA_PLUGINS_THIRD_PARTY_ACCEPT = System.getProperty("idea.qodana.thirdpartyplugins.accept").toBoolean() internal val FLEET_BACKEND_PLUGINS_THIRD_PARTY_ACCEPT = System.getProperty("fleet.backend.third-party.plugins.accept").toBoolean() +private val LOG = Logger.getInstance(PluginManagerCore::class.java) + /** * See [Plugin Model](https://youtrack.jetbrains.com/articles/IJPL-A-31/Plugin-Model) documentation. * @@ -454,8 +456,23 @@ object PluginManagerCore { fun isIncompatible(descriptor: IdeaPluginDescriptor, buildNumber: BuildNumber?): Boolean = checkBuildNumberCompatibility(descriptor, buildNumber ?: PluginManagerCore.buildNumber) != null + private val OS_ARCH_DEPENDENCY_VERSION: Regex = Regex("([\\w.]+)-(\\w+)-(\\w+)") + @ApiStatus.Internal fun getUnfulfilledOsRequirement(descriptor: IdeaPluginDescriptor): IdeaPluginOsRequirement? { + if (descriptor.dependencies.isEmpty()) { + // try to infer Arch requirement from version, some plugin repositories do not provide dependencies + val matchedVersion = descriptor.version?.let { OS_ARCH_DEPENDENCY_VERSION.matchEntire(it) } + val osTag = matchedVersion?.groupValues[2] ?: return null + + LOG.debug("Inferred OS for version: ${descriptor.version} of ${descriptor.pluginId} is $osTag") + + return OS.fromString(osTag) + .takeIf { it != OS.Other } + ?.let { IdeaPluginOsRequirement.fromOs(it) } + ?.takeIf { osReq -> !osReq.isHostOs() } + } + return descriptor.getDependencies().asSequence() .mapNotNull { dep -> IdeaPluginOsRequirement.fromModuleId(dep.pluginId).takeIf { !dep.isOptional } } .firstOrNull { osReq -> !osReq.isHostOs() } @@ -463,6 +480,19 @@ object PluginManagerCore { @ApiStatus.Internal fun getUnfulfilledCpuArchRequirement(descriptor: IdeaPluginDescriptor): PluginCpuArchRequirement? { + if (descriptor.dependencies.isEmpty()) { + // try to infer Arch requirement from version, some plugin repositories do not provide dependencies + val matchedVersion = descriptor.version?.let { OS_ARCH_DEPENDENCY_VERSION.matchEntire(it) } + val archTag = matchedVersion?.groupValues[3] ?: return null + + LOG.debug("Inferred arch for version: ${descriptor.version} of ${descriptor.pluginId} is $archTag") + + return CpuArch.fromString(archTag) + .takeIf { it != CpuArch.OTHER && it != CpuArch.UNKNOWN } + ?.let { PluginCpuArchRequirement.fromArch(it) } + ?.takeIf { osReq -> !osReq.isHostArch() } + } + return descriptor.getDependencies().asSequence() .mapNotNull { dep -> PluginCpuArchRequirement.fromPluginId(dep.pluginId).takeIf { !dep.isOptional } } .firstOrNull { osReq -> !osReq.isHostArch() } diff --git a/platform/platform-tests/testSrc/com/intellij/ide/plugins/PluginManagerTest.kt b/platform/platform-tests/testSrc/com/intellij/ide/plugins/PluginManagerTest.kt index bdbda46911c3..ccbcaf704b7a 100644 --- a/platform/platform-tests/testSrc/com/intellij/ide/plugins/PluginManagerTest.kt +++ b/platform/platform-tests/testSrc/com/intellij/ide/plugins/PluginManagerTest.kt @@ -221,6 +221,56 @@ class PluginManagerTest { } } + @Test + fun `unfulfilled os requirement is inferred from version when dependencies are empty`() { + fun descriptor(version: String?) = object : TestIdeaPluginDescriptor() { + override fun getDependencies(): List = emptyList() + override fun getVersion(): String? = version + override fun getPluginId(): PluginId = PluginId.getId("test.plugin") + } + fun assertInferred(version: String?, expected: IdeaPluginOsRequirement?) { + assertThat(PluginManagerCore.getUnfulfilledOsRequirement(descriptor(version))) + .isEqualTo(expected?.takeIf { !it.isHostOs() }) + } + + assertInferred("1.0.0-windows-amd64", IdeaPluginOsRequirement.Windows) + assertInferred("1.0.0-mac-arm64", IdeaPluginOsRequirement.Mac) + assertInferred("1.0.0-linux-amd64", IdeaPluginOsRequirement.Linux) + assertInferred("1.0.0-freebsd-amd64", IdeaPluginOsRequirement.FreeBSD) + // unrecognized OS tag maps to OS.Other and is filtered out + assertInferred("1.0.0-solaris-amd64", null) + // versions that do not match the -- pattern infer nothing + assertInferred("1.0.0", null) + assertInferred("241.SNAPSHOT", null) + // a missing version must not throw and infers nothing + assertInferred(null, null) + } + + @Test + fun `unfulfilled cpu arch requirement is inferred from version when dependencies are empty`() { + fun descriptor(version: String?) = object : TestIdeaPluginDescriptor() { + override fun getDependencies(): List = emptyList() + override fun getVersion(): String? = version + override fun getPluginId(): PluginId = PluginId.getId("test.plugin") + } + fun assertInferred(version: String?, expected: PluginCpuArchRequirement?) { + assertThat(PluginManagerCore.getUnfulfilledCpuArchRequirement(descriptor(version))) + .isEqualTo(expected?.takeIf { !it.isHostArch() }) + } + + assertInferred("1.0.0-windows-amd64", PluginCpuArchRequirement.X86_64) + assertInferred("1.0.0-windows-x86_64", PluginCpuArchRequirement.X86_64) + assertInferred("1.0.0-windows-x86", PluginCpuArchRequirement.X86) + assertInferred("1.0.0-windows-arm64", PluginCpuArchRequirement.ARM64) + assertInferred("1.0.0-windows-aarch64", PluginCpuArchRequirement.ARM64) + // unrecognized arch tag maps to CpuArch.OTHER/UNKNOWN and is filtered out + assertInferred("1.0.0-windows-sparc", null) + // versions that do not match the -- pattern infer nothing + assertInferred("1.0.0", null) + // a missing version must not throw and infers nothing + assertInferred(null, null) + } + companion object { private val testDataPath: String get() = PlatformTestUtil.getPlatformTestDataPath() + "plugins/sort"