support platform-specific custom plugin assets

GitOrigin-RevId: fbb03cf3bfda02bb2d6a5fce7b0b36e0d626d808
This commit is contained in:
Vladimir Krivosheev
2025-04-10 15:02:04 +00:00
committed by intellij-monorepo-bot
parent 85b5a669ae
commit 1eb616c8cc
10 changed files with 212 additions and 92 deletions
@@ -1,10 +1,14 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.intellij.build
import org.jetbrains.intellij.build.impl.SupportedDistribution
internal interface CustomAssetDescriptor {
/** Relative to plugin dir (not to plugin lib dir) */
val relativePath: String?
get() = null
suspend fun getSources(context: BuildContext): Sequence<Source>?
val platformSpecific: SupportedDistribution?
suspend fun getSources(context: BuildContext): Sequence<LazySource>?
}
@@ -30,6 +30,31 @@ class LazySource(
override fun toString(): String = "LazySource(name=$name, precomputedHash=$precomputedHash)"
}
data class UnpackedZipSource(
@JvmField val file: Path,
override val filter: ((String) -> Boolean),
) : Source, Comparable<ZipSource> {
override fun compareTo(other: ZipSource): Int {
return if (isWindows) file.toString().compareTo(other.file.toString()) else file.compareTo(other.file)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ZipSource) return false
if (file != other.file) return false
if (filter != other.filter) return false
return true
}
override fun hashCode(): Int {
var result = file.hashCode()
result = 31 * result + filter.hashCode()
return result
}
}
data class ZipSource(
@JvmField val file: Path,
@JvmField val isPreSignedAndExtractedCandidate: Boolean = false,
@@ -46,6 +46,7 @@ internal fun createSourceAndCacheStrategyList(sources: Collection<Source>, produ
}
}
is LazySource -> LazySourceAndCacheStrategy(source)
is UnpackedZipSource -> throw UnsupportedOperationException("UnpackedZipSource is expected to be wrapped into LazySourceAndCacheStrategy")
}
}
}
@@ -14,7 +14,6 @@ import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
@@ -37,7 +36,6 @@ import org.jetbrains.intellij.build.LinuxDistributionCustomizer
import org.jetbrains.intellij.build.MacDistributionCustomizer
import org.jetbrains.intellij.build.OsFamily
import org.jetbrains.intellij.build.PluginBuildDescriptor
import org.jetbrains.intellij.build.PluginBundlingRestrictions
import org.jetbrains.intellij.build.ProductProperties
import org.jetbrains.intellij.build.ProprietaryBuildTools
import org.jetbrains.intellij.build.ScrambleTool
@@ -54,23 +52,19 @@ import org.jetbrains.intellij.build.impl.CompilationContextImpl
import org.jetbrains.intellij.build.impl.ModuleOutputPatcher
import org.jetbrains.intellij.build.impl.PLUGIN_CLASSPATH
import org.jetbrains.intellij.build.impl.PlatformLayout
import org.jetbrains.intellij.build.impl.PluginLayout
import org.jetbrains.intellij.build.impl.asArchivedIfNeeded
import org.jetbrains.intellij.build.impl.collectIncludedPluginModules
import org.jetbrains.intellij.build.impl.collectPlatformModules
import org.jetbrains.intellij.build.impl.copyAdditionalPlugins
import org.jetbrains.intellij.build.impl.copyDistFiles
import org.jetbrains.intellij.build.impl.createIdeaPropertyFile
import org.jetbrains.intellij.build.impl.createPlatformLayout
import org.jetbrains.intellij.build.impl.generateRuntimeModuleRepositoryForDevBuild
import org.jetbrains.intellij.build.impl.getOsDistributionBuilder
import org.jetbrains.intellij.build.impl.getPluginLayoutsByJpsModuleNames
import org.jetbrains.intellij.build.impl.getToolModules
import org.jetbrains.intellij.build.impl.layoutPlatformDistribution
import org.jetbrains.intellij.build.impl.productInfo.PRODUCT_INFO_FILE_NAME
import org.jetbrains.intellij.build.impl.projectStructureMapping.DistributionFileEntry
import org.jetbrains.intellij.build.impl.projectStructureMapping.ModuleOutputEntry
import org.jetbrains.intellij.build.impl.satisfiesBundlingRequirements
import org.jetbrains.intellij.build.jarCache.LocalDiskJarCacheManager
import org.jetbrains.intellij.build.postData
import org.jetbrains.intellij.build.readSearchableOptionIndex
@@ -421,34 +415,6 @@ private suspend fun computeIdeFingerprint(
Span.current().addEvent("IDE fingerprint: $fingerprint")
}
private suspend fun buildPlugins(
request: BuildRequest,
context: BuildContext,
runDir: Path,
platformLayout: Deferred<PlatformLayout>,
artifactTask: Job,
searchableOptionSet: SearchableOptionSetDescriptor?,
buildPlatformJob: Job,
moduleOutputPatcher: ModuleOutputPatcher,
): Pair<List<Pair<PluginBuildDescriptor, List<DistributionFileEntry>>>, List<Pair<Path, List<Path>>>?> {
val bundledMainModuleNames = getBundledMainModuleNames(context, request.additionalModules)
val pluginRootDir = runDir.resolve("plugins")
val plugins = getPluginLayoutsByJpsModuleNames(bundledMainModuleNames, context.productProperties.productLayout)
.filter { isPluginApplicable(bundledMainModuleNames, plugin = it, context) }
withContext(Dispatchers.IO) {
Files.createDirectories(pluginRootDir)
}
artifactTask.join()
val pluginEntries = buildPlugins(plugins, platformLayout.await(), searchableOptionSet, context, pluginRootDir, buildPlatformJob, moduleOutputPatcher)
val additionalPlugins = copyAdditionalPlugins(context, pluginRootDir)
return pluginEntries to additionalPlugins
}
private suspend fun createBuildContext(
createProductProperties: suspend (CompilationContext) -> ProductProperties,
request: BuildRequest,
@@ -458,8 +424,8 @@ private suspend fun createBuildContext(
): BuildContext {
return coroutineScope {
val buildOptionsTemplate = request.buildOptionsTemplate
val useCompiledClassesFromProjectOutput =
buildOptionsTemplate == null || (buildOptionsTemplate.useCompiledClassesFromProjectOutput && buildOptionsTemplate.unpackCompiledClassesArchives)
val useCompiledClassesFromProjectOutput = buildOptionsTemplate == null ||
(buildOptionsTemplate.useCompiledClassesFromProjectOutput && buildOptionsTemplate.unpackCompiledClassesArchives)
val classOutDir = if (useCompiledClassesFromProjectOutput) {
request.productionClassOutput.parent
}
@@ -572,19 +538,6 @@ private suspend fun createBuildContext(
}
}
private fun isPluginApplicable(bundledMainModuleNames: Set<String>, plugin: PluginLayout, context: BuildContext): Boolean {
if (!bundledMainModuleNames.contains(plugin.mainModule)) {
return false
}
if (plugin.bundlingRestrictions == PluginBundlingRestrictions.NONE) {
return true
}
return satisfiesBundlingRequirements(plugin = plugin, osFamily = OsFamily.currentOs, arch = JvmArchitecture.currentJvmArch, context = context) ||
satisfiesBundlingRequirements(plugin = plugin, osFamily = null, arch = JvmArchitecture.currentJvmArch, context = context)
}
internal suspend fun createProductProperties(productConfiguration: ProductConfiguration, compilationContext: CompilationContext, request: BuildRequest): ProductProperties {
val classPathFiles = coroutineScope {
getBuildModules(productConfiguration).map { async { compilationContext.getModuleOutputDir(compilationContext.findRequiredModule(it)) } }.toList()
@@ -669,14 +622,6 @@ private suspend fun layoutPlatform(
return entries to sortedClassPath
}
private suspend fun getBundledMainModuleNames(context: BuildContext, additionalModules: List<String>): Set<String> {
val bundledPluginModules = context.getBundledPluginModules()
val result = LinkedHashSet<String>(bundledPluginModules.size + additionalModules.size)
result.addAll(bundledPluginModules)
result.addAll(additionalModules)
return result
}
private fun computeAdditionalModulesFingerprint(additionalModules: List<String>): String {
if (additionalModules.isEmpty()) {
return ""
@@ -1,38 +1,96 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:Suppress("PrivatePropertyName", "LiftReturnOrAssignment")
package org.jetbrains.intellij.build.dev
import io.opentelemetry.api.common.AttributeKey
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext
import org.jetbrains.intellij.build.BuildContext
import org.jetbrains.intellij.build.JvmArchitecture
import org.jetbrains.intellij.build.OsFamily
import org.jetbrains.intellij.build.PluginBuildDescriptor
import org.jetbrains.intellij.build.PluginBundlingRestrictions
import org.jetbrains.intellij.build.SearchableOptionSetDescriptor
import org.jetbrains.intellij.build.impl.*
import org.jetbrains.intellij.build.impl.DistributionBuilderState
import org.jetbrains.intellij.build.impl.ModuleOutputPatcher
import org.jetbrains.intellij.build.impl.PlatformLayout
import org.jetbrains.intellij.build.impl.PluginLayout
import org.jetbrains.intellij.build.impl.SupportedDistribution
import org.jetbrains.intellij.build.impl.buildPlugins
import org.jetbrains.intellij.build.impl.copyAdditionalPlugins
import org.jetbrains.intellij.build.impl.getPluginLayoutsByJpsModuleNames
import org.jetbrains.intellij.build.impl.handleCustomPlatformSpecificAssets
import org.jetbrains.intellij.build.impl.projectStructureMapping.DistributionFileEntry
import org.jetbrains.intellij.build.impl.satisfiesBundlingRequirements
import org.jetbrains.intellij.build.telemetry.TraceManager.spanBuilder
import org.jetbrains.intellij.build.telemetry.use
import java.nio.file.Files
import java.nio.file.Path
internal suspend fun buildPlugins(
plugins: List<PluginLayout>,
platformLayout: PlatformLayout,
searchableOptionSet: SearchableOptionSetDescriptor?,
request: BuildRequest,
context: BuildContext,
pluginRootDir: Path,
runDir: Path,
platformLayout: Deferred<PlatformLayout>,
artifactTask: Job,
searchableOptionSet: SearchableOptionSetDescriptor?,
buildPlatformJob: Job,
moduleOutputPatcher: ModuleOutputPatcher,
): List<Pair<PluginBuildDescriptor, List<DistributionFileEntry>>> {
return spanBuilder("build plugins").setAttribute(AttributeKey.longKey("count"), plugins.size.toLong()).use {
): Pair<List<Pair<PluginBuildDescriptor, List<DistributionFileEntry>>>, List<Pair<Path, List<Path>>>?> {
val bundledMainModuleNames = getBundledMainModuleNames(context, request.additionalModules)
val pluginRootDir = runDir.resolve("plugins")
val plugins = getPluginLayoutsByJpsModuleNames(bundledMainModuleNames, context.productProperties.productLayout)
.filter { isPluginApplicable(bundledMainModuleNames = bundledMainModuleNames, plugin = it, context = context) }
withContext(Dispatchers.IO) {
Files.createDirectories(pluginRootDir)
}
artifactTask.join()
val platform = platformLayout.await()
val pluginEntries = spanBuilder("build plugins").setAttribute(AttributeKey.longKey("count"), plugins.size.toLong()).use {
val targetPlatform = SupportedDistribution(OsFamily.currentOs, JvmArchitecture.currentJvmArch)
buildPlugins(
moduleOutputPatcher = moduleOutputPatcher,
plugins = plugins,
targetDir = pluginRootDir,
state = DistributionBuilderState(platform = platformLayout, pluginsToPublish = emptySet(), context = context),
state = DistributionBuilderState(platform = platform, pluginsToPublish = emptySet(), context = context),
context = context,
buildPlatformJob = buildPlatformJob,
searchableOptionSet = searchableOptionSet,
os = null,
pluginBuilt = { layout, pluginDirOrFile ->
handleCustomPlatformSpecificAssets(layout = layout, targetPlatform = targetPlatform, context = context, pluginDir = pluginDirOrFile)
},
)
}
val additionalPlugins = copyAdditionalPlugins(context, pluginRootDir)
return pluginEntries to additionalPlugins
}
private fun isPluginApplicable(bundledMainModuleNames: Set<String>, plugin: PluginLayout, context: BuildContext): Boolean {
if (!bundledMainModuleNames.contains(plugin.mainModule)) {
return false
}
if (plugin.bundlingRestrictions == PluginBundlingRestrictions.NONE) {
return true
}
return satisfiesBundlingRequirements(plugin = plugin, osFamily = OsFamily.currentOs, arch = JvmArchitecture.currentJvmArch, context = context) ||
satisfiesBundlingRequirements(plugin = plugin, osFamily = null, arch = JvmArchitecture.currentJvmArch, context = context)
}
private suspend fun getBundledMainModuleNames(context: BuildContext, additionalModules: List<String>): Set<String> {
val bundledPluginModules = context.getBundledPluginModules()
val result = LinkedHashSet<String>(bundledPluginModules.size + additionalModules.size)
result.addAll(bundledPluginModules)
result.addAll(additionalModules)
return result
}
@@ -714,27 +714,6 @@ internal suspend fun buildPlugins(
return entries
}
private suspend fun buildPlatformSpecificPluginResources(
plugin: PluginLayout,
targetDirs: List<Pair<SupportedDistribution, Path>>,
context: BuildContext,
) {
for ((dist, generators) in plugin.platformResourceGenerators) {
val targetPath = targetDirs.firstOrNull { it.first == dist }?.second ?: continue
val pluginDir = targetPath.resolve(plugin.directoryName)
val relativePluginDir = context.paths.buildOutputDir.relativize(pluginDir).toString()
for (generator in generators) {
spanBuilder("plugin platform-specific resources")
.setAttribute("path", relativePluginDir)
.setAttribute("os", dist.os.toString())
.setAttribute("arch", dist.arch.toString())
.use {
generator(pluginDir, context)
}
}
}
}
private const val PLUGINS_DIRECTORY = "plugins"
private const val LIB_DIRECTORY = "lib"
@@ -364,6 +364,10 @@ class JarPackager private constructor(
private suspend fun handleCustomAssets(layout: PluginLayout, jarAsset: Lazy<AssetDescriptor>) {
for (customAsset in layout.customAssets) {
if (customAsset.platformSpecific != null) {
continue
}
val relativePath = customAsset.relativePath
if (relativePath == null) {
customAsset.getSources(context)?.let { jarAsset.value.sources.addAll(it) }
@@ -20,7 +20,6 @@ import org.jetbrains.intellij.build.JvmArchitecture
import org.jetbrains.intellij.build.LazySource
import org.jetbrains.intellij.build.OsFamily
import org.jetbrains.intellij.build.PluginBundlingRestrictions
import org.jetbrains.intellij.build.Source
import org.jetbrains.intellij.build.io.copyDir
import org.jetbrains.intellij.build.io.copyFileToDir
import java.nio.file.FileSystemException
@@ -105,7 +104,7 @@ class PluginLayout(val mainModule: String, @Internal @JvmField val auto: Boolean
private set
val hasPlatformSpecificResources: Boolean
get() = platformResourceGenerators.isNotEmpty()
get() = platformResourceGenerators.isNotEmpty() || customAssets.any { it.platformSpecific != null }
fun getMainJarName(): String = mainJarName
@@ -238,7 +237,21 @@ class PluginLayout(val mainModule: String, @Internal @JvmField val auto: Boolean
fun withCustomAsset(lazySourceSupplier: (context: BuildContext) -> LazySource?) {
layout.customAssets += object : CustomAssetDescriptor {
override suspend fun getSources(context: BuildContext): Sequence<Source>? {
override val platformSpecific: SupportedDistribution?
get() = null
override suspend fun getSources(context: BuildContext): Sequence<LazySource>? {
return sequenceOf(lazySourceSupplier(context) ?: return null)
}
}
}
fun withCustomAsset(platform: SupportedDistribution, lazySourceSupplier: (context: BuildContext) -> LazySource?) {
layout.customAssets += object : CustomAssetDescriptor {
override val platformSpecific: SupportedDistribution?
get() = platform
override suspend fun getSources(context: BuildContext): Sequence<LazySource>? {
return sequenceOf(lazySourceSupplier(context) ?: return null)
}
}
@@ -0,0 +1,87 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.intellij.build.impl
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.intellij.build.BuildContext
import org.jetbrains.intellij.build.UnpackedZipSource
import org.jetbrains.intellij.build.io.W_OVERWRITE
import org.jetbrains.intellij.build.io.ZipEntryProcessorResult
import org.jetbrains.intellij.build.io.readZipFile
import org.jetbrains.intellij.build.io.writeToFileChannelFully
import org.jetbrains.intellij.build.telemetry.TraceManager.spanBuilder
import org.jetbrains.intellij.build.telemetry.use
import java.nio.channels.FileChannel
import java.nio.file.Files
import java.nio.file.Path
internal suspend fun buildPlatformSpecificPluginResources(
plugin: PluginLayout,
targetDirs: List<Pair<SupportedDistribution, Path>>,
context: BuildContext,
) {
for ((dist, generators) in plugin.platformResourceGenerators) {
val targetPath = targetDirs.firstOrNull { it.first == dist }?.second ?: continue
val pluginDir = targetPath.resolve(plugin.directoryName)
val relativePluginDir = context.paths.buildOutputDir.relativize(pluginDir).toString()
for (generator in generators) {
spanBuilder("plugin platform-specific resources")
.setAttribute("path", relativePluginDir)
.setAttribute("os", dist.os.toString())
.setAttribute("arch", dist.arch.toString())
.use {
generator(pluginDir, context)
}
}
}
for ((platform, pluginDir) in targetDirs) {
handleCustomPlatformSpecificAssets(layout = plugin, targetPlatform = platform, context = context, pluginDir = pluginDir)
}
}
@ApiStatus.Internal
fun unpackTrustedZip(
source: UnpackedZipSource,
rootDir: Path,
createdParents: MutableSet<Path>,
) {
readZipFile(source.file) { name, dataProvider ->
if (!source.filter(name)) {
return@readZipFile ZipEntryProcessorResult.CONTINUE
}
val file = rootDir.resolve(name)
val parent = file.parent
if (createdParents.add(parent)) {
Files.createDirectories(parent)
}
val data = dataProvider()
FileChannel.open(file, W_OVERWRITE).use {
writeToFileChannelFully(channel = it, data = data)
}
ZipEntryProcessorResult.CONTINUE
}
}
internal suspend fun handleCustomPlatformSpecificAssets(layout: PluginLayout, targetPlatform: SupportedDistribution, context: BuildContext, pluginDir: Path) {
for (customAsset in layout.customAssets) {
val platformSpecific = customAsset.platformSpecific ?: continue
if (platformSpecific != targetPlatform) {
continue
}
val rootDir = customAsset.relativePath?.let { pluginDir.resolve(it) } ?: pluginDir
val lazySources = customAsset.getSources(context) ?: continue
val createdParents = HashSet<Path>()
for (lazySource in lazySources) {
for (source in lazySource.getSources()) {
require(source is UnpackedZipSource) {
"Only UnpackedZipSource is supported for custom plugin platform-specific assets, got $source for $customAsset"
}
unpackTrustedZip(source = source, rootDir = rootDir, createdParents = createdParents)
}
}
}
}
@@ -184,6 +184,10 @@ private suspend fun writeSource(
)
}
}
is UnpackedZipSource -> {
throw UnsupportedOperationException("UnpackedZipSource is not supported")
}
}
}