diff --git a/plugins/devkit/devkit-core/src/projectRoots/IntelliJPlatformProduct.java b/plugins/devkit/devkit-core/src/projectRoots/IntelliJPlatformProduct.java index 9c21d6eff7ca..62f5f13e4109 100644 --- a/plugins/devkit/devkit-core/src/projectRoots/IntelliJPlatformProduct.java +++ b/plugins/devkit/devkit-core/src/projectRoots/IntelliJPlatformProduct.java @@ -1,4 +1,4 @@ -// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.jetbrains.idea.devkit.projectRoots; import com.intellij.util.PlatformUtils; @@ -10,6 +10,7 @@ import org.jetbrains.annotations.Nullable; import java.util.Objects; public enum IntelliJPlatformProduct { + // sync myCdnCoordinates with IntelliJPlatformAuxiliaryArtifactProvider.CDN_GROUPS IDEA("IU", "IntelliJ IDEA", null, "com.jetbrains.intellij.idea:idea", "idea:idea"), IDEA_IU("IU", "IntelliJ IDEA", null, "com.jetbrains.intellij.idea:ideaIU", "idea:ideaIU"), IDEA_IC("IC", "IntelliJ IDEA Community Edition", PlatformUtils.IDEA_CE_PREFIX, "com.jetbrains.intellij.idea:ideaIC", "idea:ideaIC"), diff --git a/plugins/devkit/intellij.devkit.gradle.tooling/resources/META-INF/services/com.intellij.gradle.toolingExtension.impl.model.dependencyModel.auxiliary.AuxiliaryArtifactProvider b/plugins/devkit/intellij.devkit.gradle.tooling/resources/META-INF/services/com.intellij.gradle.toolingExtension.impl.model.dependencyModel.auxiliary.AuxiliaryArtifactProvider new file mode 100644 index 000000000000..8bd7d8c84508 --- /dev/null +++ b/plugins/devkit/intellij.devkit.gradle.tooling/resources/META-INF/services/com.intellij.gradle.toolingExtension.impl.model.dependencyModel.auxiliary.AuxiliaryArtifactProvider @@ -0,0 +1,2 @@ +# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +org.jetbrains.idea.devkit.gradle.tooling.IntelliJPlatformAuxiliaryArtifactProvider diff --git a/plugins/devkit/intellij.devkit.gradle.tooling/src/org/jetbrains/idea/devkit/gradle/tooling/IntelliJPlatformAuxiliaryArtifactProvider.java b/plugins/devkit/intellij.devkit.gradle.tooling/src/org/jetbrains/idea/devkit/gradle/tooling/IntelliJPlatformAuxiliaryArtifactProvider.java new file mode 100644 index 000000000000..24078d13ecfa --- /dev/null +++ b/plugins/devkit/intellij.devkit.gradle.tooling/src/org/jetbrains/idea/devkit/gradle/tooling/IntelliJPlatformAuxiliaryArtifactProvider.java @@ -0,0 +1,188 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.idea.devkit.gradle.tooling; + +import com.intellij.gradle.toolingExtension.impl.model.dependencyDownloadPolicyModel.GradleDependencyDownloadPolicy; +import com.intellij.gradle.toolingExtension.impl.model.dependencyModel.auxiliary.AuxiliaryArtifactProvider; +import com.intellij.gradle.toolingExtension.impl.model.dependencyModel.auxiliary.AuxiliaryConfigurationArtifacts; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.component.ComponentIdentifier; +import org.gradle.api.artifacts.component.ModuleComponentIdentifier; +import org.gradle.api.artifacts.result.ResolvedComponentResult; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.util.*; + +/** + * Provides source artifacts for IntelliJ Platform dependencies. + *

+ * IntelliJ Platform sources are published under different Maven coordinates + * than the main artifacts. For example, PhpStorm sources come from IntelliJ IDEA Community (IC). + * This provider maps platform dependency coordinates to the correct source coordinates + * and resolves them via detached Gradle configurations. + *

+ * This code is run on the Gradle daemon side. + */ +@SuppressWarnings("IO_FILE_USAGE") // API uses java.io.File +public final class IntelliJPlatformAuxiliaryArtifactProvider implements AuxiliaryArtifactProvider { + + private static final String JETBRAINS_INTELLIJ_PREFIX = "com.jetbrains.intellij."; + + @SuppressWarnings("SSBasedInspection") // don't use platform utils here + private static final Set CDN_GROUPS = new HashSet<>(Arrays.asList( + // sync with IntelliJPlatformProduct + "idea", + "ruby", + "python", + "webide", + "webstorm", + "cpp", + "datagrip", + "rider", + "go", + "com.google.android.studio", + "idea/code-with-me", + "idea/gateway", + "aqua", + "rustrover", + "mps" + )); + + @Override + public @NotNull AuxiliaryConfigurationArtifacts resolve( + @NotNull Project project, + @NotNull Configuration configuration, + @NotNull GradleDependencyDownloadPolicy policy + ) { + if (!policy.isDownloadSources()) { + return AuxiliaryConfigurationArtifacts.EMPTY; + } + + Map> sources = new LinkedHashMap<>(); + + Set components; + try { + components = configuration.getIncoming().getResolutionResult().getAllComponents(); + } + catch (Exception e) { + return AuxiliaryConfigurationArtifacts.EMPTY; + } + + for (ResolvedComponentResult component : components) { + ComponentIdentifier id = component.getId(); + if (!(id instanceof ModuleComponentIdentifier)) continue; + ModuleComponentIdentifier moduleId = (ModuleComponentIdentifier)id; + + String group = moduleId.getGroup(); + if (!isIntelliJPlatformComponent(group)) continue; + + String name = moduleId.getModule(); + String version = moduleId.getVersion(); + String actualVersion = IntelliJPlatformSourceCoordinates.extractActualVersion(version); + int majorVersion = IntelliJPlatformSourceCoordinates.extractMajorVersion(actualVersion); + + String sourceCoordinates = resolveSourceCoordinates(group, name, version, majorVersion); + if (sourceCoordinates == null) continue; + + String sourceNotation = sourceCoordinates + ":" + actualVersion + ":sources"; + File sourceFile = resolveSourceArtifact(project, sourceNotation); + if (sourceFile == null && majorVersion > 0) { + // Try ranged version fallback: find closest published version in the same major cycle + String rangedNotation = sourceCoordinates + ":[" + majorVersion + "," + actualVersion + "]!!" + actualVersion + ":sources"; + sourceFile = resolveSourceArtifact(project, rangedNotation); + } + if (sourceFile == null && majorVersion > 0) { + // Try SNAPSHOT fallback + String snapshotNotation = sourceCoordinates + ":" + majorVersion + "-SNAPSHOT:sources"; + sourceFile = resolveSourceArtifact(project, snapshotNotation); + } + + if (sourceFile != null) { + sources.put(id, Collections.singleton(sourceFile)); + } + } + + if (sources.isEmpty()) { + return AuxiliaryConfigurationArtifacts.EMPTY; + } + return new AuxiliaryConfigurationArtifacts(sources, Collections.emptyMap()); + } + + private static boolean isIntelliJPlatformComponent(@NotNull String group) { + return group.startsWith(JETBRAINS_INTELLIJ_PREFIX) + || CDN_GROUPS.contains(group) + || "com.jetbrains.plugins".equals(group) + || "localIde".equals(group) + || "bundledPlugin".equals(group) + || "bundledModule".equals(group); + } + + private static @Nullable String resolveSourceCoordinates( + @NotNull String group, @NotNull String name, @NotNull String version, int majorVersion + ) { + // PyCharm variants → PC sources + if (("com.jetbrains.intellij.pycharm".equals(group) && ("pycharmPY".equals(name) || "pycharmPC".equals(name))) + || "python".equals(group)) { + return IntelliJPlatformSourceCoordinates.PYCHARM_COMMUNITY_SOURCES; + } + + if (isIdea("ideaIU", group, name)) { + return IntelliJPlatformSourceCoordinates.ideaUltimateSources(majorVersion); + } + if (isIdea("ideaIC", group, name)) { + return IntelliJPlatformSourceCoordinates.defaultPlatformSources(majorVersion); + } + // IDEA (unified coordinates for 253+) + if (isIdea("idea", group, name)) { + return "com.jetbrains.intellij.idea:idea"; + } + + // localIde: artifact name is the product code (e.g., "IC", "IU") + if ("localIde".equals(group)) { + return IntelliJPlatformSourceCoordinates.sourceCoordinatesForProductCode(name, majorVersion); + } + + // bundledPlugin / bundledModule: product code is in the version prefix (e.g., "IC-243.21565.193") + if ("bundledPlugin".equals(group) || "bundledModule".equals(group)) { + String productCode = IntelliJPlatformSourceCoordinates.extractProductCode(version); + if (productCode != null) { + return IntelliJPlatformSourceCoordinates.sourceCoordinatesForProductCode(productCode, majorVersion); + } + return IntelliJPlatformSourceCoordinates.defaultPlatformSources(majorVersion); + } + + // Non-bundled plugins from JetBrains Maven repo + if ("com.jetbrains.plugins".equals(group)) { + return IntelliJPlatformSourceCoordinates.defaultPlatformSources(majorVersion); + } + + // All other IntelliJ Platform products → IC/idea sources + if (group.startsWith(JETBRAINS_INTELLIJ_PREFIX) || CDN_GROUPS.contains(group)) { + return IntelliJPlatformSourceCoordinates.defaultPlatformSources(majorVersion); + } + + return null; + } + + private static boolean isIdea(@NotNull String expectedName, @NotNull String group, @NotNull String name) { + return ("com.jetbrains.intellij.idea".equals(group) && expectedName.equals(name)) + || ("idea".equals(group) && expectedName.equals(name)); + } + + private static @Nullable File resolveSourceArtifact(@NotNull Project project, @NotNull String notation) { + try { + Configuration detached = project.getConfigurations().detachedConfiguration(project.getDependencies().create(notation)); + detached.setTransitive(false); + Set files = detached.resolve(); + if (files.size() == 1) { + return files.iterator().next(); + } + return null; + } + catch (Exception e) { + return null; + } + } +} diff --git a/plugins/devkit/intellij.devkit.gradle.tooling/src/org/jetbrains/idea/devkit/gradle/tooling/IntelliJPlatformSourceCoordinates.java b/plugins/devkit/intellij.devkit.gradle.tooling/src/org/jetbrains/idea/devkit/gradle/tooling/IntelliJPlatformSourceCoordinates.java new file mode 100644 index 000000000000..3c5c14279827 --- /dev/null +++ b/plugins/devkit/intellij.devkit.gradle.tooling/src/org/jetbrains/idea/devkit/gradle/tooling/IntelliJPlatformSourceCoordinates.java @@ -0,0 +1,118 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.idea.devkit.gradle.tooling; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Shared utility for resolving IntelliJ Platform source artifact coordinates. + *

+ * Used by both the Gradle daemon side ({@link IntelliJPlatformAuxiliaryArtifactProvider}) + * and the IDE side ({@code IntelliJPlatformAttachSourcesProvider}). + */ +public final class IntelliJPlatformSourceCoordinates { + + public static final String PYCHARM_COMMUNITY_SOURCES = "com.jetbrains.intellij.pycharm:pycharmPC"; + + /** + * Source coordinates for IDEA Ultimate (version-dependent: ideaIC → ideaIU → idea). + */ + public static @NotNull String ideaUltimateSources(int majorVersion) { + if (majorVersion >= 253) return "com.jetbrains.intellij.idea:idea"; + if (majorVersion >= 242) return "com.jetbrains.intellij.idea:ideaIU"; + return "com.jetbrains.intellij.idea:ideaIC"; + } + + /** + * Source coordinates for IDEA Community or any other IntelliJ Platform product. + */ + public static @NotNull String defaultPlatformSources(int majorVersion) { + if (majorVersion >= 253) return "com.jetbrains.intellij.idea:idea"; + return "com.jetbrains.intellij.idea:ideaIC"; + } + + /** + * Extracts the actual version from a possibly prefixed version string. + * Strips product-code prefix (e.g., "IC-") and build metadata suffix (e.g., "+445"). + *

+ * Examples: + *

+ */ + public static @NotNull String extractActualVersion(@NotNull String version) { + int dashIndex = version.indexOf('-'); + if (dashIndex >= 0) { + version = version.substring(dashIndex + 1); + } + int plusIndex = version.indexOf('+'); + if (plusIndex >= 0) { + version = version.substring(0, plusIndex); + } + return version; + } + + /** + * Extracts the product code prefix from a version string, if present. + *

+ * Examples: + *

+ */ + public static @Nullable String extractProductCode(@NotNull String version) { + int dashIndex = version.indexOf('-'); + if (dashIndex > 0) { + String prefix = version.substring(0, dashIndex); + // Product codes are short alphabetic strings (e.g., IC, IU, PY, PC) + if (prefix.chars().allMatch(Character::isLetter)) { + return prefix; + } + } + return null; + } + + /** + * Maps a product code to the appropriate source coordinates. + */ + public static @NotNull String sourceCoordinatesForProductCode(@NotNull String productCode, int majorVersion) { + switch (productCode) { + case "IU": + return ideaUltimateSources(majorVersion); + case "PY": + case "PC": + return PYCHARM_COMMUNITY_SOURCES; + default: + return defaultPlatformSources(majorVersion); + } + } + + /** + * Extracts major version number from a version string. + * Handles both release format ("2024.1.1") and build number format ("243.21565.193"). + * + * @return major version number, or -1 if the version string cannot be parsed + */ + public static int extractMajorVersion(@NotNull String version) { + try { + String[] parts = version.split("\\."); + int first = Integer.parseInt(parts[0]); + if (first >= 2000) { + // Release version format: 2024.1.1 + int minor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; + return (first - 2000) * 10 + minor; + } + else { + // Build number format: 243.21565.193 + return first; + } + } + catch (NumberFormatException e) { + return -1; + } + } +} \ No newline at end of file diff --git a/plugins/devkit/intellij.devkit.gradle/resources/intellij.devkit.gradle.xml b/plugins/devkit/intellij.devkit.gradle/resources/intellij.devkit.gradle.xml index fc9f1c55a61a..1478cda8ef85 100644 --- a/plugins/devkit/intellij.devkit.gradle/resources/intellij.devkit.gradle.xml +++ b/plugins/devkit/intellij.devkit.gradle/resources/intellij.devkit.gradle.xml @@ -12,6 +12,7 @@ + diff --git a/plugins/devkit/intellij.devkit.gradle/src/DevKitGradleProjectResolverExtension.kt b/plugins/devkit/intellij.devkit.gradle/src/DevKitGradleProjectResolverExtension.kt new file mode 100644 index 000000000000..1c570b078cd2 --- /dev/null +++ b/plugins/devkit/intellij.devkit.gradle/src/DevKitGradleProjectResolverExtension.kt @@ -0,0 +1,10 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.idea.devkit.gradle + +import org.jetbrains.idea.devkit.gradle.tooling.IntelliJPlatformAuxiliaryArtifactProvider +import org.jetbrains.plugins.gradle.service.project.AbstractProjectResolverExtension + +internal class DevKitGradleProjectResolverExtension : AbstractProjectResolverExtension() { + override fun getToolingExtensionsClasses(): Set> = + setOf(IntelliJPlatformAuxiliaryArtifactProvider::class.java) +} diff --git a/plugins/devkit/intellij.devkit.gradle/src/IntelliJPlatformAttachSourcesProvider.kt b/plugins/devkit/intellij.devkit.gradle/src/IntelliJPlatformAttachSourcesProvider.kt index 1bae5c5bb8b1..8878981c329e 100644 --- a/plugins/devkit/intellij.devkit.gradle/src/IntelliJPlatformAttachSourcesProvider.kt +++ b/plugins/devkit/intellij.devkit.gradle/src/IntelliJPlatformAttachSourcesProvider.kt @@ -1,4 +1,4 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.jetbrains.idea.devkit.gradle import com.intellij.codeInsight.AttachSourcesProvider @@ -23,6 +23,7 @@ import com.intellij.workspaceModel.ide.legacyBridge.findModule import org.jetbrains.annotations.PropertyKey import org.jetbrains.idea.devkit.projectRoots.IntelliJPlatformProduct import org.jetbrains.idea.devkit.run.ProductInfo +import org.jetbrains.idea.devkit.gradle.tooling.IntelliJPlatformSourceCoordinates import org.jetbrains.idea.devkit.run.loadProductInfo import org.jetbrains.plugins.gradle.execution.build.CachedModuleDataFinder import org.jetbrains.plugins.gradle.service.project.GradleNotification @@ -80,7 +81,7 @@ internal class IntelliJPlatformAttachSourcesProvider : AttachSourcesProvider { // IntelliJ Platform dependency, such as `com.jetbrains.intellij.idea:ideaIC:2023.2.7`, `idea:ideaIC:aarch64:2024.3`, or `idea:ideaIC:2023.2.7` product != null -> resolveIntelliJPlatformAction(psiFile, coordinates.version) - // IntelliJ Platform bundled plugin, such as `localIde:IC:2023.2.7+445` + // IntelliJ Platform local installation, such as `localIde:IC:2023.2.7+445` coordinates.groupId == "localIde" -> createAttachLocalPlatformSourcesAction(psiFile, coordinates) // IntelliJ Platform bundled plugin, such as `bundledPlugin:org.intellij.groovy:IC-243.21565.193`, `bundledPlugin:Git4Idea:2023.2.7+445` @@ -115,7 +116,7 @@ internal class IntelliJPlatformAttachSourcesProvider : AttachSourcesProvider { val buildNumber = productInfo.buildNumber val majorVersion = buildNumber.substringBefore('.').toInt() val rangedVersion = "[$majorVersion,$buildNumber]!!$buildNumber" - val productCoordinates = resolveProductCoordinates(product, majorVersion) ?: return null + val productCoordinates = resolveProductCoordinates(product, majorVersion) return when { // We're handing LSP API class, but IU is lower than 242 -> attach a standalone sources file @@ -133,7 +134,7 @@ internal class IntelliJPlatformAttachSourcesProvider : AttachSourcesProvider { * @param coordinates The Maven coordinates of the IntelliJ Platform whose sources need to be attached. */ private fun createAttachLocalPlatformSourcesAction(psiFile: PsiFile, coordinates: MavenCoordinates) = - resolveIntelliJPlatformAction(psiFile, coordinates.version.substringAfter('-').substringBefore('+')) + resolveIntelliJPlatformAction(psiFile, IntelliJPlatformSourceCoordinates.extractActualVersion(coordinates.version)) /** * Creates an action to attach sources of bundled plugins for the IntelliJ Platform. @@ -143,7 +144,7 @@ internal class IntelliJPlatformAttachSourcesProvider : AttachSourcesProvider { */ private fun createAttachBundledPluginSourcesAction(psiFile: PsiFile, coordinates: MavenCoordinates) = createAttachSourcesArchiveAction(psiFile, ApiSourceArchive.entries.firstOrNull { it.id == coordinates.artifactId }) - ?: resolveIntelliJPlatformAction(psiFile, coordinates.version.substringAfter('-').substringBefore('+')) + ?: resolveIntelliJPlatformAction(psiFile, IntelliJPlatformSourceCoordinates.extractActualVersion(coordinates.version)) /** * Creates an action to attach sources of bundled modules for the IntelliJ Platform. @@ -256,20 +257,22 @@ internal class IntelliJPlatformAttachSourcesProvider : AttachSourcesProvider { } } - downloadAndAttach(primaryNotation) { - downloadAndAttach(fallbackNotation) { - downloadAndAttach(nightlySnapshotNotation) { + downloadAndAttach(primaryNotation, onFailure = { + downloadAndAttach(fallbackNotation, onFailure = { + downloadAndAttach(nightlySnapshotNotation, onFailure = { GradleNotification.gradleNotificationGroup - .createNotification(title = GradleBundle.message("gradle.notifications.sources.download.failed.title"), - content = GradleBundle.message("gradle.notifications.sources.download.failed.content", primaryNotation), - NotificationType.WARNING) + .createNotification( + title = GradleBundle.message("gradle.notifications.sources.download.failed.title"), + content = GradleBundle.message("gradle.notifications.sources.download.failed.content", primaryNotation), + type = NotificationType.WARNING + ) .setDisplayId("gradle.notifications.sources.download.failed") .notify(project) executionResult.setRejected() - } - } - } + }) + }) + }) return executionResult } @@ -339,20 +342,11 @@ internal class IntelliJPlatformAttachSourcesProvider : AttachSourcesProvider { private fun resolveProductCoordinates(product: IntelliJPlatformProduct, majorVersion: Int) = when (product) { - // For PyCharm Community and PyCharm, we use PC sources. - IntelliJPlatformProduct.PYCHARM, IntelliJPlatformProduct.PYCHARM_PC -> IntelliJPlatformProduct.PYCHARM_PC - - // IntelliJ IDEA Ultimate has sources published since 242; otherwise we use IC. - IntelliJPlatformProduct.IDEA -> when { - majorVersion >= 253 -> IntelliJPlatformProduct.IDEA - majorVersion >= 242 -> IntelliJPlatformProduct.IDEA_IU - else -> IntelliJPlatformProduct.IDEA_IC - } - - // Any other IntelliJ Platform should use IC - else -> when { - majorVersion >= 253 -> IntelliJPlatformProduct.IDEA - else -> IntelliJPlatformProduct.IDEA_IC - } - }.mavenCoordinates + IntelliJPlatformProduct.PYCHARM, IntelliJPlatformProduct.PYCHARM_PC -> + IntelliJPlatformSourceCoordinates.PYCHARM_COMMUNITY_SOURCES + IntelliJPlatformProduct.IDEA -> + IntelliJPlatformSourceCoordinates.ideaUltimateSources(majorVersion) + else -> + IntelliJPlatformSourceCoordinates.defaultPlatformSources(majorVersion) + } }