IJI-1455 reporting build steps of distribution build test as TeamCity tests

GitOrigin-RevId: a7837fe388570ae8be635dd57ed1fe182441c3cf
This commit is contained in:
Dmitriy.Panov
2024-07-26 18:31:11 +02:00
committed by intellij-monorepo-bot
parent ecce5f78de
commit fe99f89930
16 changed files with 392 additions and 283 deletions

View File

@@ -25,7 +25,9 @@ abstract class BuildMessageLoggerBase : BuildMessageLogger() {
val errorsString = (message as CompilationErrorsLogMessage).errorMessages.joinToString(separator = "\n")
Span.current().addEvent("compilation errors (${message.compilerName}):\n$errorsString")
}
LogMessage.Kind.BUILD_CANCEL -> throw BuildScriptsLoggedError(message.text)
LogMessage.Kind.BUILD_CANCEL, LogMessage.Kind.BUILD_PROBLEM -> {
throw BuildScriptsLoggedError(message.text)
}
else -> {
if (shouldBePrinted(message.kind)) {
Span.current().addEvent(message.text)

View File

@@ -104,7 +104,7 @@ class TeamCityBuildMessageLogger : BuildMessageLogger() {
print(ServiceMessageTypes.COMPILATION_FINISHED, "compiler" to compiler)
}
DEBUG -> {} //debug messages are printed to a separate file available in the build artifacts
BUILD_PROBLEM -> {
BUILD_PROBLEM -> { // The text is limited to 4000 symbols and will be truncated if the limit is exceeded
check(message is BuildProblemLogMessage) {
"Unexpected build problem message type: ${message::class.java.canonicalName}"
}

View File

@@ -5,7 +5,6 @@ import com.intellij.openapi.application.PathManager
import com.intellij.platform.buildScripts.testFramework.createBuildOptionsForTest
import com.intellij.platform.buildScripts.testFramework.runEssentialPluginsTest
import com.intellij.platform.buildScripts.testFramework.runTestBuild
import com.intellij.platform.buildScripts.testFramework.spanName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import org.jetbrains.intellij.build.BuildPaths.Companion.COMMUNITY_ROOT
@@ -20,7 +19,7 @@ class IdeaCommunityBuildTest {
val productProperties = IdeaCommunityProperties(COMMUNITY_ROOT.communityRoot)
runTestBuild(
homeDir = homePath,
traceSpanName = testInfo.spanName,
testInfo = testInfo,
productProperties = productProperties,
) {
it.classOutDir = System.getProperty(BuildOptions.PROJECT_CLASSES_OUTPUT_DIRECTORY_PROPERTY) ?: "$homePath/out/classes"
@@ -31,16 +30,17 @@ class IdeaCommunityBuildTest {
fun jpsStandalone(testInfo: TestInfo) {
val homePath = PathManager.getHomeDirFor(javaClass)!!
runBlocking(Dispatchers.Default) {
val productProperties = IdeaCommunityProperties(COMMUNITY_ROOT.communityRoot)
val options = createBuildOptionsForTest(productProperties = productProperties, homeDir = homePath, skipDependencySetup = true)
val context = BuildContextImpl.createContext(
projectHome = homePath,
productProperties = productProperties,
setupTracer = false,
options = options,
)
runTestBuild(context = context, traceSpanName = testInfo.spanName) {
buildCommunityStandaloneJpsBuilder(targetDir = context.paths.artifactDir.resolve("jps"), context = context)
runTestBuild(testInfo, context = {
val productProperties = IdeaCommunityProperties(COMMUNITY_ROOT.communityRoot)
val options = createBuildOptionsForTest(productProperties = productProperties, homeDir = homePath, skipDependencySetup = true, testInfo)
BuildContextImpl.createContext(
projectHome = homePath,
productProperties = productProperties,
setupTracer = false,
options = options,
)
}) {
buildCommunityStandaloneJpsBuilder(targetDir = it.paths.artifactDir.resolve("jps"), context = it)
}
}
}

View File

@@ -8,6 +8,7 @@ import kotlinx.collections.immutable.PersistentMap
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.serialization.Serializable
import org.jetbrains.intellij.build.productRunner.IntellijProductRunner
import org.jetbrains.jps.model.module.JpsModule
@@ -149,12 +150,27 @@ interface BuildContext : CompilationContext {
suspend inline fun <T> BuildContext.executeStep(spanBuilder: SpanBuilder,
stepId: String,
crossinline step: suspend CoroutineScope.(Span) -> T): T? {
if (isStepSkipped(stepId)) {
spanBuilder.startSpan().addEvent("skip '$stepId' step").end()
return null
}
else {
return spanBuilder.useWithScope(Dispatchers.IO, step)
return spanBuilder.useWithScope(Dispatchers.IO) { span ->
try {
options.buildStepListener.onStart(stepId, messages)
if (isStepSkipped(stepId)) {
span.addEvent("skip '$stepId' step")
options.buildStepListener.onSkipping(stepId, messages)
null
}
else {
coroutineScope {
step(span)
}
}
}
catch (failure: Throwable) {
options.buildStepListener.onFailure(stepId, failure, messages)
null
}
finally {
options.buildStepListener.onCompletion(stepId, messages)
}
}
}

View File

@@ -478,6 +478,10 @@ data class BuildOptions(
@ApiStatus.Internal
var useReleaseCycleRelatedBundlingRestrictionsForContentReport: Boolean = true
@set:TestOnly
@ApiStatus.Internal
var buildStepListener: BuildStepListener = BuildStepListener()
init {
val targetOsId = System.getProperty(TARGET_OS_PROPERTY, OS_ALL).lowercase()
targetOs = when {

View File

@@ -0,0 +1,31 @@
// Copyright 2000-2024 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.annotations.ApiStatus
import org.jetbrains.intellij.build.BuildOptions.Companion.BUILD_STEPS_TO_SKIP_PROPERTY
import org.jetbrains.intellij.build.dependencies.TeamCityHelper
/**
* > Not to be confused with [TeamCity build steps](https://www.jetbrains.com/help/teamcity/configuring-build-steps.html)
*
* Listens to lifecycle events of each build step launched with [org.jetbrains.intellij.build.executeStep].
*/
@ApiStatus.Internal
open class BuildStepListener {
open suspend fun onStart(stepId: String, messages: BuildMessages) {}
open suspend fun onSkipping(stepId: String, messages: BuildMessages) {}
open suspend fun onFailure(stepId: String, failure: Throwable, messages: BuildMessages) {
val description = buildString {
append("'$stepId' build step failed")
if (TeamCityHelper.isUnderTeamCity) {
append(" (Please don't mute this problem!") // mute scope may be too broad muting similar failures in other build configuration
append(" If you really need to ignore it, you may either mark this build as green or add '$stepId' to 'system.${BUILD_STEPS_TO_SKIP_PROPERTY}')")
}
append(": ")
append(failure.stackTraceToString())
}
messages.reportBuildProblem(description, identity = stepId)
}
open suspend fun onCompletion(stepId: String, messages: BuildMessages) {}
}

View File

@@ -24,7 +24,7 @@ internal fun CoroutineScope.createStatisticsRecorderBundledMetadataProviderTask(
val featureUsageStatisticsPropertiesList = context.proprietaryBuildTools.featureUsageStatisticsProperties ?: return null
return createSkippableJob(
spanBuilder("bundle a default version of feature usage statistics"),
taskId = BuildOptions.FUS_METADATA_BUNDLE_STEP,
stepId = BuildOptions.FUS_METADATA_BUNDLE_STEP,
context = context
) {
for (featureUsageStatisticsProperties in featureUsageStatisticsPropertiesList) {

View File

@@ -7,29 +7,23 @@ import com.intellij.util.system.OS
import io.opentelemetry.api.trace.SpanBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.launch
import org.jetbrains.intellij.build.BuildContext
import org.jetbrains.intellij.build.CompilationContext
import org.jetbrains.intellij.build.OsFamily
import org.jetbrains.intellij.build.telemetry.TraceManager.spanBuilder
import org.jetbrains.intellij.build.executeStep
import org.jetbrains.intellij.build.io.copyDir
import java.nio.file.Path
import java.util.function.Predicate
inline fun CoroutineScope.createSkippableJob(spanBuilder: SpanBuilder,
taskId: String,
stepId: String,
context: BuildContext,
crossinline task: suspend () -> Unit): Job? {
if (context.isStepSkipped(taskId)) {
spanBuilder.startSpan().addEvent("skip").end()
return null
}
else {
return launch {
spanBuilder.useWithScope {
task()
}
crossinline task: suspend () -> Unit): Job {
return launch {
context.executeStep(spanBuilder, stepId) {
task()
}
}
}

View File

@@ -246,65 +246,63 @@ internal class DistributionForOsTaskResult(
)
private suspend fun buildOsSpecificDistributions(context: BuildContext): List<DistributionForOsTaskResult> {
if (context.isStepSkipped(BuildOptions.OS_SPECIFIC_DISTRIBUTIONS_STEP)) {
Span.current().addEvent("skip step", Attributes.of(AttributeKey.stringKey("name"), "build OS-specific distributions"))
return emptyList()
}
return context.executeStep(spanBuilder("build OS-specific distributions"), BuildOptions.OS_SPECIFIC_DISTRIBUTIONS_STEP) {
setLastModifiedTime(context.paths.distAllDir, context)
setLastModifiedTime(context.paths.distAllDir, context)
if (context.isMacCodeSignEnabled) {
withContext(Dispatchers.IO) {
for (file in Files.newDirectoryStream(context.paths.distAllDir).use { stream ->
stream.filter { !it.endsWith("help") && !it.endsWith("license") && !it.endsWith("lib") }
}) {
launch {
// todo exclude plugins - layoutAdditionalResources should perform codesign -
// that's why we process files and zip in plugins (but not JARs)
// and also kotlin compiler includes JNA
recursivelySignMacBinaries(root = file, context = context)
if (context.isMacCodeSignEnabled) {
withContext(Dispatchers.IO) {
for (file in Files.newDirectoryStream(context.paths.distAllDir).use { stream ->
stream.filter { !it.endsWith("help") && !it.endsWith("license") && !it.endsWith("lib") }
}) {
launch {
// todo exclude plugins - layoutAdditionalResources should perform codesign -
// that's why we process files and zip in plugins (but not JARs)
// and also kotlin compiler includes JNA
recursivelySignMacBinaries(root = file, context = context)
}
}
}
}
}
val ideaPropertyFileContent = createIdeaPropertyFile(context)
val ideaPropertyFileContent = createIdeaPropertyFile(context)
spanBuilder("Adjust executable permissions on common dist").use {
val matchers = SUPPORTED_DISTRIBUTIONS.mapNotNull {
getOsDistributionBuilder(it.os, null, context)
}.flatMap { builder ->
JvmArchitecture.entries.flatMap { arch ->
builder.generateExecutableFilesMatchers(includeRuntime = true, arch = arch).keys
}
}
updateExecutablePermissions(context.paths.distAllDir, matchers)
}
return supervisorScope {
SUPPORTED_DISTRIBUTIONS.mapNotNull { (os, arch) ->
if (!context.shouldBuildDistributionForOS(os, arch)) {
return@mapNotNull null
}
val builder = getOsDistributionBuilder(os = os, ideaProperties = ideaPropertyFileContent, context = context) ?: return@mapNotNull null
val stepId = "${os.osId} ${arch.name}"
if (context.options.buildStepsToSkip.contains(stepId)) {
Span.current().addEvent("skip step", Attributes.of(AttributeKey.stringKey("id"), stepId))
return@mapNotNull null
}
async {
spanBuilder(stepId).useWithScope {
val osAndArchSpecificDistDirectory = getOsAndArchSpecificDistDirectory(osFamily = os, arch = arch, context = context)
builder.buildArtifacts(osAndArchSpecificDistPath = osAndArchSpecificDistDirectory, arch = arch)
checkClassFiles(root = osAndArchSpecificDistDirectory, context = context, isDistAll = false)
DistributionForOsTaskResult(builder = builder, arch = arch, outDir = osAndArchSpecificDistDirectory)
spanBuilder("Adjust executable permissions on common dist").use {
val matchers = SUPPORTED_DISTRIBUTIONS.mapNotNull {
getOsDistributionBuilder(it.os, null, context)
}.flatMap { builder ->
JvmArchitecture.entries.flatMap { arch ->
builder.generateExecutableFilesMatchers(includeRuntime = true, arch = arch).keys
}
}
updateExecutablePermissions(context.paths.distAllDir, matchers)
}
}.collectCompletedOrError()
supervisorScope {
SUPPORTED_DISTRIBUTIONS.mapNotNull { (os, arch) ->
if (!context.shouldBuildDistributionForOS(os, arch)) {
return@mapNotNull null
}
val builder = getOsDistributionBuilder(os = os, ideaProperties = ideaPropertyFileContent, context = context) ?: return@mapNotNull null
val stepId = "${os.osId} ${arch.name}"
if (context.options.buildStepsToSkip.contains(stepId)) {
Span.current().addEvent("skip step", Attributes.of(AttributeKey.stringKey("id"), stepId))
return@mapNotNull null
}
async {
spanBuilder(stepId).useWithScope {
val osAndArchSpecificDistDirectory = getOsAndArchSpecificDistDirectory(osFamily = os, arch = arch, context = context)
builder.buildArtifacts(osAndArchSpecificDistPath = osAndArchSpecificDistDirectory, arch = arch)
checkClassFiles(root = osAndArchSpecificDistDirectory, context = context, isDistAll = false)
DistributionForOsTaskResult(builder = builder, arch = arch, outDir = osAndArchSpecificDistDirectory)
}
}
}
}.collectCompletedOrError()
} ?: emptyList()
}
// call only after supervisorScope
@@ -377,66 +375,71 @@ internal fun collectModulesToCompileForDistribution(context: BuildContext): Muta
private suspend fun compileModulesForDistribution(context: BuildContext): DistributionBuilderState {
val compilationTasks = CompilationTasks.create(context)
collectModulesToCompileForDistribution(context).let {
compilationTasks.compileModules(moduleNames = it)
}
val moduleNames = collectModulesToCompileForDistribution(context)
compilationTasks.compileModules(moduleNames)
val productLayout = context.productProperties.productLayout
val pluginsToPublish = getPluginLayoutsByJpsModuleNames(modules = productLayout.pluginModulesToPublish, productLayout = productLayout)
filterPluginsToPublish(plugins = pluginsToPublish, context = context)
var enabledPluginModules = getEnabledPluginModules(pluginsToPublish = pluginsToPublish, context = context)
val enabledPluginModules = getEnabledPluginModules(pluginsToPublish = pluginsToPublish, context = context)
// computed only based on a bundled and plugins to publish lists, compatible plugins are not taken in an account by intention
val projectLibrariesUsedByPlugins = computeProjectLibsUsedByPlugins(enabledPluginModules = enabledPluginModules, context = context)
if (context.shouldBuildDistributions()) {
if (context.isStepSkipped(BuildOptions.PROVIDED_MODULES_LIST_STEP)) {
Span.current().addEvent("skip collecting compatible plugins because ${BuildOptions.PROVIDED_MODULES_LIST_STEP} was skipped")
return context.executeStep(spanBuilder("collecting compatible plugins"), BuildOptions.PROVIDED_MODULES_LIST_STEP) {
if (!context.shouldBuildDistributions()) {
it.addEvent("skipped, no need to build distributions")
return@executeStep null
}
val providedModuleFile = context.paths.artifactDir.resolve("${context.applicationInfo.productCode}-builtinModules.json")
val platform = createPlatformLayout(context = context)
val moduleNames = getModulesForPluginsToPublish(platform = platform, pluginsToPublish = pluginsToPublish)
compilationTasks.compileModules(moduleNames)
val builtinModuleData = spanBuilder("build provided module list").useWithScope {
Files.deleteIfExists(providedModuleFile)
// start the product in headless mode using com.intellij.ide.plugins.BundledPluginsLister
context.createProductRunner().runProduct(args = listOf("listBundledPlugins", providedModuleFile.toString()))
context.productProperties.customizeBuiltinModules(context = context, builtinModulesFile = providedModuleFile)
try {
val builtinModuleData = readBuiltinModulesFile(file = providedModuleFile)
context.builtinModule = builtinModuleData
builtinModuleData
}
catch (_: NoSuchFileException) {
throw IllegalStateException("Failed to build provided modules list: $providedModuleFile doesn't exist")
}
}
context.notifyArtifactBuilt(artifactPath = providedModuleFile)
if (!productLayout.buildAllCompatiblePlugins) {
val distState = DistributionBuilderState(platform = platform, pluginsToPublish = pluginsToPublish, context = context)
buildProjectArtifacts(platform = distState.platform, enabledPluginModules = enabledPluginModules, compilationTasks = compilationTasks, context = context)
distState
}
else {
val providedModuleFile = context.paths.artifactDir.resolve("${context.applicationInfo.productCode}-builtinModules.json")
val platform = createPlatformLayout(context = context)
getModulesForPluginsToPublish(platform = platform, pluginsToPublish = pluginsToPublish).let {
compilationTasks.compileModules(moduleNames = it)
}
val builtinModuleData = spanBuilder("build provided module list").useWithScope {
Files.deleteIfExists(providedModuleFile)
// start the product in headless mode using com.intellij.ide.plugins.BundledPluginsLister
context.createProductRunner().runProduct(args = listOf("listBundledPlugins", providedModuleFile.toString()))
context.productProperties.customizeBuiltinModules(context = context, builtinModulesFile = providedModuleFile)
try {
val builtinModuleData = readBuiltinModulesFile(file = providedModuleFile)
context.builtinModule = builtinModuleData
builtinModuleData
}
catch (_: NoSuchFileException) {
throw IllegalStateException("Failed to build provided modules list: $providedModuleFile doesn\'t exist")
}
}
context.notifyArtifactBuilt(artifactPath = providedModuleFile)
if (!productLayout.buildAllCompatiblePlugins) {
val distState = DistributionBuilderState(platform = platform, pluginsToPublish = pluginsToPublish, context = context)
buildProjectArtifacts(platform = distState.platform, enabledPluginModules = enabledPluginModules, compilationTasks = compilationTasks, context = context)
return distState
}
collectCompatiblePluginsToPublish(builtinModuleData = builtinModuleData, result = pluginsToPublish, context = context)
filterPluginsToPublish(plugins = pluginsToPublish, context = context)
// update enabledPluginModules to reflect changes in pluginsToPublish - used for buildProjectArtifacts
enabledPluginModules = getEnabledPluginModules(pluginsToPublish = pluginsToPublish, context = context)
val enabledPluginModules = getEnabledPluginModules(pluginsToPublish = pluginsToPublish, context = context)
distributionState(context, pluginsToPublish, projectLibrariesUsedByPlugins, enabledPluginModules)
}
}
} ?: distributionState(context, pluginsToPublish, projectLibrariesUsedByPlugins, enabledPluginModules)
}
private suspend fun distributionState(
context: BuildContext,
pluginsToPublish: Set<PluginLayout>,
projectLibrariesUsedByPlugins: SortedSet<ProjectLibraryData>,
enabledPluginModules: Set<String>,
): DistributionBuilderState {
val platform = createPlatformLayout(projectLibrariesUsedByPlugins = projectLibrariesUsedByPlugins, context = context)
val distState = DistributionBuilderState(platform = platform, pluginsToPublish = pluginsToPublish, context = context)
distState.getModulesForPluginsToPublish().let {
compilationTasks.compileModules(moduleNames = it)
}
val moduleNames = distState.getModulesForPluginsToPublish()
val compilationTasks = CompilationTasks.create(context)
compilationTasks.compileModules(moduleNames)
buildProjectArtifacts(platform = distState.platform, enabledPluginModules = enabledPluginModules, compilationTasks = compilationTasks, context = context)
return distState
}
@@ -553,7 +556,7 @@ private fun CoroutineScope.createMavenArtifactJob(context: BuildContext, distrib
}
}
private fun checkProductProperties(context: BuildContextImpl) {
private suspend fun checkProductProperties(context: BuildContextImpl) {
checkProductLayout(context)
val properties = context.productProperties
@@ -604,7 +607,7 @@ private fun checkProductProperties(context: BuildContextImpl) {
listOfNotNull(macCustomizer.icnsPathForAlternativeIconForEAP),
"productProperties.macCustomizer.icnsPathForAlternativeIconForEAP"
)
if (!context.isStepSkipped(BuildOptions.MAC_DMG_STEP)) {
context.executeStep(spanBuilder("check .dmg images"), BuildOptions.MAC_DMG_STEP) {
checkMandatoryPath(macCustomizer.dmgImagePath, "productProperties.macCustomizer.dmgImagePath")
checkPaths(listOfNotNull(macCustomizer.dmgImagePathForEAP), "productProperties.macCustomizer.dmgImagePathForEAP")
}
@@ -867,28 +870,30 @@ private fun buildCrossPlatformZip(distResults: List<DistributionForOsTaskResult>
private suspend fun checkClassFiles(root: Path, context: BuildContext, isDistAll: Boolean) {
// version checking patterns are only for dist all (all non-os and non-arch specific files)
if (!isDistAll || context.isStepSkipped(BuildOptions.VERIFY_CLASS_FILE_VERSIONS)) {
if (!isDistAll) {
return
}
val versionCheckerConfig = context.productProperties.versionCheckerConfig
val forbiddenSubPaths = context.productProperties.forbiddenClassFileSubPaths
val forbiddenSubPathExceptions = context.productProperties.forbiddenClassFileSubPathExceptions
if (forbiddenSubPaths.isNotEmpty()) {
val forbiddenString = forbiddenSubPaths.let { "(${it.size}): ${it.joinToString()}" }
val exceptionsString = forbiddenSubPathExceptions.let { "(${it.size}): ${it.joinToString()}" }
Span.current().addEvent("checkClassFiles: forbiddenSubPaths $forbiddenString, exceptions $exceptionsString")
}
else {
Span.current().addEvent("checkClassFiles: forbiddenSubPaths: EMPTY (no scrambling checks will be done)")
}
context.executeStep(spanBuilder("checkClassFiles"), BuildOptions.VERIFY_CLASS_FILE_VERSIONS) {
val versionCheckerConfig = context.productProperties.versionCheckerConfig
val forbiddenSubPaths = context.productProperties.forbiddenClassFileSubPaths
val forbiddenSubPathExceptions = context.productProperties.forbiddenClassFileSubPathExceptions
if (forbiddenSubPaths.isNotEmpty()) {
val forbiddenString = forbiddenSubPaths.let { "(${it.size}): ${it.joinToString()}" }
val exceptionsString = forbiddenSubPathExceptions.let { "(${it.size}): ${it.joinToString()}" }
it.addEvent("forbiddenSubPaths $forbiddenString, exceptions $exceptionsString")
}
else {
it.addEvent("forbiddenSubPaths: EMPTY (no scrambling checks will be done)")
}
if (versionCheckerConfig.isNotEmpty() || forbiddenSubPaths.isNotEmpty()) {
checkClassFiles(versionCheckConfig = versionCheckerConfig, forbiddenSubPaths = forbiddenSubPaths, forbiddenSubPathExceptions = forbiddenSubPathExceptions, root = root)
}
if (versionCheckerConfig.isNotEmpty() || forbiddenSubPaths.isNotEmpty()) {
checkClassFiles(versionCheckConfig = versionCheckerConfig, forbiddenSubPaths = forbiddenSubPaths, forbiddenSubPathExceptions = forbiddenSubPathExceptions, root = root)
}
if (forbiddenSubPaths.isNotEmpty()) {
Span.current().addEvent("checkClassFiles: SUCCESS for forbiddenSubPaths at '$root': ${forbiddenSubPaths.joinToString()}")
if (forbiddenSubPaths.isNotEmpty()) {
it.addEvent("SUCCESS for forbiddenSubPaths at '$root': ${forbiddenSubPaths.joinToString()}")
}
}
}

View File

@@ -397,13 +397,9 @@ internal suspend fun buildNonBundledPlugins(
searchableOptionSet: SearchableOptionSetDescriptor?,
context: BuildContext,
): List<Pair<PluginBuildDescriptor, List<DistributionFileEntry>>> {
return spanBuilder("build non-bundled plugins").setAttribute("count", pluginsToPublish.size.toLong()).useWithScope { span ->
return context.executeStep(spanBuilder("build non-bundled plugins").setAttribute("count", pluginsToPublish.size.toLong()), BuildOptions.NON_BUNDLED_PLUGINS_STEP) {
if (pluginsToPublish.isEmpty()) {
return@useWithScope emptyList()
}
if (context.isStepSkipped(BuildOptions.NON_BUNDLED_PLUGINS_STEP)) {
span.addEvent("skip")
return@useWithScope emptyList()
return@executeStep emptyList()
}
val nonBundledPluginsArtifacts = context.paths.artifactDir.resolve("${context.applicationInfo.productCode}-plugins")
@@ -473,39 +469,44 @@ internal suspend fun buildNonBundledPlugins(
generatePluginRepositoryMetaFile(list.filter { it.pluginZip.startsWith(autoUploadingDir) }, autoUploadingDir, context)
}
if (!context.isStepSkipped(BuildOptions.VALIDATE_PLUGINS_TO_BE_PUBLISHED)) {
for (plugin in pluginSpecs) {
launch {
validatePlugin(path = plugin.pluginZip, context = context)
}
}
}
validatePlugins(context, pluginSpecs)
mappings
} ?: emptyList()
}
private suspend fun validatePlugins(context: BuildContext, pluginSpecs: Collection<PluginRepositorySpec>) {
context.executeStep(spanBuilder("plugins validation"), BuildOptions.VALIDATE_PLUGINS_TO_BE_PUBLISHED) { span ->
for (plugin in pluginSpecs) {
val path = plugin.pluginZip
if (Files.notExists(path)) {
span.addEvent("doesn't exist, skipped", Attributes.of(AttributeKey.stringKey("path"), "$path"))
continue
}
launch {
validatePlugin(path, context, span)
}
}
}
}
private suspend fun validatePlugin(path: Path, context: BuildContext) {
spanBuilder("plugin validation").setAttribute("path", path.toString()).useWithScope { span ->
if (Files.notExists(path)) {
span.addEvent("path doesn't exist, skipped")
return@useWithScope
}
val pluginManager = IdePluginManager.createManager()
val result = pluginManager.createPlugin(path, validateDescriptor = true)
// todo fix AddStatisticsEventLogListenerTemporary
val id = when (result) {
is PluginCreationSuccess -> result.plugin.pluginId
is PluginCreationFail -> (pluginManager.createPlugin(path, validateDescriptor = false) as? PluginCreationSuccess)?.plugin?.pluginId
}
val problems = context.productProperties.validatePlugin(id, result, context)
if (problems.isNotEmpty()) {
context.messages.reportBuildProblem(problems.joinToString(
private fun validatePlugin(path: Path, context: BuildContext, span: Span) {
val pluginManager = IdePluginManager.createManager()
val result = pluginManager.createPlugin(path, validateDescriptor = true)
// todo fix AddStatisticsEventLogListenerTemporary
val id = when (result) {
is PluginCreationSuccess -> result.plugin.pluginId
is PluginCreationFail -> (pluginManager.createPlugin(path, validateDescriptor = false) as? PluginCreationSuccess)?.plugin?.pluginId
}
val problems = context.productProperties.validatePlugin(id, result, context)
if (problems.isNotEmpty()) {
span.addEvent("failed", Attributes.of(AttributeKey.stringKey("path"), "$path"))
context.messages.reportBuildProblem(
problems.joinToString(
prefix = "${id ?: path}: ",
separator = ". ",
), identity = "${id ?: path}")
}
), identity = "${id ?: path}"
)
}
}
@@ -871,7 +872,7 @@ private suspend fun scramble(platform: PlatformLayout, context: BuildContext) {
}
}
private fun CoroutineScope.createBuildBrokenPluginListJob(context: BuildContext): Job? {
private fun CoroutineScope.createBuildBrokenPluginListJob(context: BuildContext): Job {
val buildString = context.fullBuildNumber
return createSkippableJob(
spanBuilder("build broken plugin list").setAttribute("buildNumber", buildString),
@@ -885,7 +886,7 @@ private fun CoroutineScope.createBuildBrokenPluginListJob(context: BuildContext)
}
}
private fun CoroutineScope.createBuildThirdPartyLibraryListJob(entries: Sequence<DistributionFileEntry>, context: BuildContext): Job? {
private fun CoroutineScope.createBuildThirdPartyLibraryListJob(entries: Sequence<DistributionFileEntry>, context: BuildContext): Job {
return createSkippableJob(spanBuilder("generate table of licenses for used third-party libraries"),
BuildOptions.THIRD_PARTY_LIBRARIES_LIST_STEP, context) {
val generator = createLibraryLicensesListGenerator(

View File

@@ -121,14 +121,16 @@ class LinuxDistributionBuilder(
}
}
if (tarGzPath != null && !context.isStepSkipped(BuildOptions.REPAIR_UTILITY_BUNDLE_STEP)) {
val tempTar = Files.createTempDirectory(context.paths.tempDir, "tar-")
try {
unTar(tarGzPath, tempTar)
RepairUtilityBuilder.generateManifest(context, unpackedDistribution = tempTar.resolve(rootDirectoryName), OsFamily.LINUX, arch)
}
finally {
NioFiles.deleteRecursively(tempTar)
if (tarGzPath != null ) {
context.executeStep(spanBuilder("bundle repair utility"), BuildOptions.REPAIR_UTILITY_BUNDLE_STEP) {
val tempTar = Files.createTempDirectory(context.paths.tempDir, "tar-")
try {
unTar(tarGzPath, tempTar)
RepairUtilityBuilder.generateManifest(context, unpackedDistribution = tempTar.resolve(rootDirectoryName), OsFamily.LINUX, arch)
}
finally {
NioFiles.deleteRecursively(tempTar)
}
}
}
}

View File

@@ -4,7 +4,6 @@
package org.jetbrains.intellij.build
import io.opentelemetry.api.common.AttributeKey
import io.opentelemetry.api.trace.Span
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.serialization.Contextual
@@ -68,42 +67,38 @@ internal suspend fun buildSearchableOptions(
context: BuildContext,
systemProperties: VmProperties = VmProperties(emptyMap()),
): SearchableOptionSetDescriptor? {
val span = Span.current()
if (context.isStepSkipped(BuildOptions.SEARCHABLE_OPTIONS_INDEX_STEP)) {
span.addEvent("skip building searchable options index")
return null
return context.executeStep(spanBuilder("building searchable options index"), BuildOptions.SEARCHABLE_OPTIONS_INDEX_STEP) { span ->
val targetDirectory = context.paths.searchableOptionDir
// bundled maven is also downloaded during traverseUI execution in an external process,
// making it fragile to call more than one traverseUI at the same time (in the reproducibility test, for example),
// so it's pre-downloaded with proper synchronization
coroutineScope {
launch {
BundledMavenDownloader.downloadMaven4Libs(context.paths.communityHomeDirRoot)
}
launch {
BundledMavenDownloader.downloadMaven3Libs(context.paths.communityHomeDirRoot)
}
launch {
BundledMavenDownloader.downloadMavenDistribution(context.paths.communityHomeDirRoot)
}
launch {
BundledMavenDownloader.downloadMavenTelemetryDependencies(context.paths.communityHomeDirRoot)
}
}
// Start the product in headless mode using com.intellij.ide.ui.search.TraverseUIStarter.
// It'll process all UI elements in the `Settings` dialog and build an index for them.
productRunner.runProduct(
args = listOf("traverseUI", targetDirectory.toString(), "true"),
additionalVmProperties = systemProperties + VmProperties(mapOf("idea.l10n.keys" to "only")),
timeout = DEFAULT_TIMEOUT,
)
val index = readSearchableOptionIndex(targetDirectory)
span.setAttribute(AttributeKey.longKey("moduleCountWithSearchableOptions"), index.index.size)
span.setAttribute(AttributeKey.stringArrayKey("modulesWithSearchableOptions"), index.index.keys.toList())
index
}
val targetDirectory = context.paths.searchableOptionDir
// bundled maven is also downloaded during traverseUI execution in an external process,
// making it fragile to call more than one traverseUI at the same time (in the reproducibility test, for example),
// so it's pre-downloaded with proper synchronization
coroutineScope {
launch {
BundledMavenDownloader.downloadMaven4Libs(context.paths.communityHomeDirRoot)
}
launch {
BundledMavenDownloader.downloadMaven3Libs(context.paths.communityHomeDirRoot)
}
launch {
BundledMavenDownloader.downloadMavenDistribution(context.paths.communityHomeDirRoot)
}
launch {
BundledMavenDownloader.downloadMavenTelemetryDependencies(context.paths.communityHomeDirRoot)
}
}
// Start the product in headless mode using com.intellij.ide.ui.search.TraverseUIStarter.
// It'll process all UI elements in the `Settings` dialog and build an index for them.
productRunner.runProduct(
args = listOf("traverseUI", targetDirectory.toString(), "true"),
additionalVmProperties = systemProperties + VmProperties(mapOf("idea.l10n.keys" to "only")),
timeout = DEFAULT_TIMEOUT,
)
val index = readSearchableOptionIndex(targetDirectory)
span.setAttribute(AttributeKey.longKey("moduleCountWithSearchableOptions"), index.index.size)
span.setAttribute(AttributeKey.stringArrayKey("modulesWithSearchableOptions"), index.index.keys.toList())
return index
}

View File

@@ -0,0 +1,50 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.buildScripts.testFramework
import jetbrains.buildServer.messages.serviceMessages.ServiceMessageTypes
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.intellij.build.BuildMessages
import org.jetbrains.intellij.build.BuildStepListener
import org.jetbrains.intellij.build.logging.TeamCityBuildMessageLogger.Companion.SpanAwareServiceMessage
import org.junit.jupiter.api.TestInfo
/**
* See [BuildStepListener].
*
* Reports each build step launched with [org.jetbrains.intellij.build.executeStep] as a TeamCity test:
* * such a test must be started in its own dedicated [flow](https://www.jetbrains.com/help/teamcity/service-messages.html#Message+FlowId), see [org.jetbrains.intellij.build.executeStep]
* * each flow started withing a test should be a transitive subflow of that test's own flow, see [org.jetbrains.intellij.build.logging.TeamCityBuildMessageLogger.withFlow]
*
* Otherwise, tests will not be correctly displayed in a TeamCity build log.
*/
@ApiStatus.Internal
open class BuildStepTeamCityListener(testInfo: TestInfo) : BuildStepListener() {
private val testName: String = "${testInfo.testClass.get().canonicalName}.${testInfo.testMethod.orElseThrow().name}"
private fun reportTestEvent(testEvent: String, stepId: String, vararg attributes: Pair<String, String>) {
println(SpanAwareServiceMessage(testEvent, "name" to "$testName($stepId)", *attributes))
}
override suspend fun onStart(stepId: String, messages: BuildMessages) {
super.onStart(stepId, messages)
reportTestEvent(ServiceMessageTypes.TEST_STARTED, stepId)
messages.warning("This test is automatically generated from the build step '$stepId'")
messages.warning("To run it locally, please invoke '$testName'")
}
override suspend fun onSkipping(stepId: String, messages: BuildMessages) {
super.onSkipping(stepId, messages)
reportTestEvent(ServiceMessageTypes.TEST_IGNORED, stepId)
}
override suspend fun onFailure(stepId: String, failure: Throwable, messages: BuildMessages) {
// no need to throw the build step failure, just reporting it to TeamCity as a test failure,
// providing an option to mute it without muting the main test
reportTestEvent(ServiceMessageTypes.TEST_FAILED, stepId, "message" to "${failure.message}", "details" to failure.stackTraceToString())
}
override suspend fun onCompletion(stepId: String, messages: BuildMessages) {
super.onCompletion(stepId, messages)
reportTestEvent(ServiceMessageTypes.TEST_FINISHED, stepId)
}
}

View File

@@ -8,18 +8,21 @@ import org.jetbrains.intellij.build.ProductProperties
import org.jetbrains.intellij.build.ProprietaryBuildTools
import org.jetbrains.intellij.build.impl.readBuiltinModulesFile
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.TestInfo
import java.nio.file.Path
/**
* Checks that frontend distribution (ex JetBrains Client) described by [frontendProperties] can be built successfully.
*/
fun runTestBuildForFrontend(homePath: Path, frontendProperties: ProductProperties, buildTools: ProprietaryBuildTools,
traceSpanName: String, softly: SoftAssertions) {
fun runTestBuildForFrontend(
homePath: Path, frontendProperties: ProductProperties, buildTools: ProprietaryBuildTools,
testInfo: TestInfo, softly: SoftAssertions,
) {
runTestBuild(
homeDir = homePath,
productProperties = frontendProperties,
buildTools = buildTools,
traceSpanName = traceSpanName,
testInfo = testInfo,
onSuccess = { context ->
verifyBuiltInModules(context.paths.artifactDir.resolve("${context.applicationInfo.productCode}-builtinModules.json"))
assertTrue(context.useModularLoader) { "Frontend distribution must use the modular loader, but $frontendProperties doesn't use it" }

View File

@@ -18,8 +18,10 @@ import kotlinx.coroutines.runBlocking
import org.assertj.core.api.SoftAssertions
import org.jetbrains.intellij.build.*
import org.jetbrains.intellij.build.telemetry.TraceManager.spanBuilder
import org.jetbrains.intellij.build.dependencies.TeamCityHelper.isUnderTeamCity
import org.jetbrains.intellij.build.impl.BuildContextImpl
import org.jetbrains.intellij.build.impl.buildDistributions
import org.jetbrains.intellij.build.telemetry.JaegerJsonSpanExporterManager
import org.jetbrains.intellij.build.telemetry.TraceManager
import org.jetbrains.intellij.build.telemetry.useWithScope
import org.junit.jupiter.api.TestInfo
@@ -28,10 +30,10 @@ import java.net.http.HttpConnectTimeoutException
import java.nio.file.Files
import java.nio.file.Path
fun createBuildOptionsForTest(productProperties: ProductProperties, homeDir: Path, skipDependencySetup: Boolean = false): BuildOptions {
fun createBuildOptionsForTest(productProperties: ProductProperties, homeDir: Path, skipDependencySetup: Boolean = false, testInfo: TestInfo? = null): BuildOptions {
val outDir = createTestBuildOutDir(productProperties)
val options = BuildOptions(cleanOutDir = false, useCompiledClassesFromProjectOutput = true, jarCacheDir = homeDir.resolve("out/dev-run/jar-cache"))
customizeBuildOptionsForTest(options = options, outDir = outDir, skipDependencySetup = skipDependencySetup)
customizeBuildOptionsForTest(options = options, outDir = outDir, skipDependencySetup = skipDependencySetup, testInfo = testInfo)
return options
}
@@ -39,13 +41,13 @@ fun createTestBuildOutDir(productProperties: ProductProperties): Path {
return FileUtil.createTempDirectory("test-build-${productProperties.baseFileName}", null, false).toPath()
}
private inline fun createBuildOptionsForTest(productProperties: ProductProperties, homeDir: Path, customizer: (BuildOptions) -> Unit): BuildOptions {
val options = createBuildOptionsForTest(productProperties = productProperties, homeDir = homeDir)
private inline fun createBuildOptionsForTest(productProperties: ProductProperties, homeDir: Path, testInfo: TestInfo, customizer: (BuildOptions) -> Unit): BuildOptions {
val options = createBuildOptionsForTest(productProperties = productProperties, homeDir = homeDir, testInfo = testInfo)
customizer(options)
return options
}
fun customizeBuildOptionsForTest(options: BuildOptions, outDir: Path, skipDependencySetup: Boolean = false) {
fun customizeBuildOptionsForTest(options: BuildOptions, outDir: Path, skipDependencySetup: Boolean = false, testInfo: TestInfo?) {
options.skipDependencySetup = skipDependencySetup
options.isTestBuild = true
@@ -56,11 +58,15 @@ fun customizeBuildOptionsForTest(options: BuildOptions, outDir: Path, skipDepend
BuildOptions.WIN_SIGN_STEP,
BuildOptions.MAC_SIGN_STEP,
BuildOptions.MAC_NOTARIZE_STEP,
BuildOptions.MAC_DMG_STEP,
)
options.buildUnixSnaps = false
options.outRootDir = outDir
options.useCompiledClassesFromProjectOutput = true
options.compilationLogEnabled = false
if (testInfo != null && isUnderTeamCity) {
options.buildStepListener = BuildStepTeamCityListener(testInfo)
}
}
suspend inline fun createBuildContext(
@@ -78,7 +84,7 @@ suspend inline fun createBuildContext(
fun runTestBuild(
homePath: Path,
productProperties: ProductProperties,
traceSpanName: String,
testInfo: TestInfo,
buildTools: ProprietaryBuildTools,
buildOptionsCustomizer: (BuildOptions) -> Unit = {},
) {
@@ -86,7 +92,7 @@ fun runTestBuild(
homeDir = homePath,
productProperties = productProperties,
buildTools = buildTools,
traceSpanName = traceSpanName,
testInfo = testInfo,
isReproducibilityTestAllowed = true,
buildOptionsCustomizer = buildOptionsCustomizer,
)
@@ -96,28 +102,29 @@ fun runTestBuild(
homeDir: Path,
productProperties: ProductProperties,
buildTools: ProprietaryBuildTools = ProprietaryBuildTools.DUMMY,
traceSpanName: String,
testInfo: TestInfo,
isReproducibilityTestAllowed: Boolean = true,
build: suspend (context: BuildContext) -> Unit = { buildDistributions(it) },
onSuccess: suspend (context: BuildContext) -> Unit = {},
build: suspend (BuildContext) -> Unit = { buildDistributions(it) },
onSuccess: suspend (BuildContext) -> Unit = {},
buildOptionsCustomizer: (BuildOptions) -> Unit = {}
) = runBlocking(Dispatchers.Default) {
if (isReproducibilityTestAllowed) {
if (isReproducibilityTestAllowed && BuildArtifactsReproducibilityTest.isEnabled) {
val reproducibilityTest = BuildArtifactsReproducibilityTest()
repeat(reproducibilityTest.iterations) { iterationNumber ->
launch {
val buildContext = BuildContextImpl.createContext(
projectHome = homeDir,
productProperties = productProperties,
proprietaryBuildTools = buildTools,
setupTracer = false,
options = createBuildOptionsForTest(productProperties = productProperties, homeDir = homeDir, customizer = buildOptionsCustomizer).also {
reproducibilityTest.configure(it)
},
)
doRunTestBuild(
context = buildContext,
traceSpanName = "#$iterationNumber",
context = {
BuildContextImpl.createContext(
projectHome = homeDir,
productProperties = productProperties,
proprietaryBuildTools = buildTools,
setupTracer = false,
options = createBuildOptionsForTest(productProperties = productProperties, homeDir = homeDir, testInfo, customizer = buildOptionsCustomizer).also {
reproducibilityTest.configure(it)
},
)
},
traceSpanName = "${testInfo.spanName}#$iterationNumber",
writeTelemetry = false,
build = { context ->
build(context)
@@ -130,15 +137,17 @@ fun runTestBuild(
}
else {
doRunTestBuild(
context = BuildContextImpl.createContext(
projectHome = homeDir,
productProperties = productProperties,
proprietaryBuildTools = buildTools,
setupTracer = false,
options = createBuildOptionsForTest(productProperties = productProperties, homeDir = homeDir, customizer = buildOptionsCustomizer),
),
context = {
BuildContextImpl.createContext(
projectHome = homeDir,
productProperties = productProperties,
proprietaryBuildTools = buildTools,
setupTracer = false,
options = createBuildOptionsForTest(productProperties = productProperties, homeDir = homeDir, testInfo, customizer = buildOptionsCustomizer),
)
},
writeTelemetry = true,
traceSpanName = traceSpanName,
traceSpanName = testInfo.spanName,
build = { context ->
build(context)
onSuccess(context)
@@ -149,33 +158,30 @@ fun runTestBuild(
// FIXME: test reproducibility
suspend fun runTestBuild(
context: BuildContext,
traceSpanName: String,
build: suspend (context: BuildContext) -> Unit = { buildDistributions(it) }
testInfo: TestInfo,
context: suspend () -> BuildContext,
build: suspend (BuildContext) -> Unit = { buildDistributions(it) }
) {
doRunTestBuild(context = context, traceSpanName = traceSpanName, writeTelemetry = true, build = build)
doRunTestBuild(context = context, traceSpanName = testInfo.spanName, writeTelemetry = true, build = build)
}
private val defaultLogFactory = Logger.getFactory()
private suspend fun doRunTestBuild(context: BuildContext, traceSpanName: String?, writeTelemetry: Boolean, build: suspend (context: BuildContext) -> Unit) {
context.cleanupJarCache()
val traceFile = if (writeTelemetry) {
val traceFile = TestLoggerFactory.getTestLogDir().resolve("${context.productProperties.baseFileName}-$traceSpanName-trace.json")
JaegerJsonSpanExporterManager.setOutput(traceFile, addShutDownHook = false)
traceFile
}
else {
null
}
val outDir = context.paths.buildOutputDir
private suspend fun doRunTestBuild(context: suspend () -> BuildContext, traceSpanName: String, writeTelemetry: Boolean, build: suspend (context: BuildContext) -> Unit) {
var outDir: Path? = null
var traceFile: Path? = null
var error: Throwable? = null
try {
spanBuilder(traceSpanName ?: "test build of ${context.productProperties.baseFileName}")
.setAttribute("outDir", outDir.toString())
.useWithScope { span ->
spanBuilder(traceSpanName).useWithScope { span ->
val context = context()
context.cleanupJarCache()
outDir = context.paths.buildOutputDir
span.setAttribute("outDir", "$outDir")
if (writeTelemetry) {
traceFile = TestLoggerFactory.getTestLogDir().resolve("${context.productProperties.baseFileName}-$traceSpanName-trace.json").also {
JaegerJsonSpanExporterManager.setOutput(it, addShutDownHook = false)
}
}
try {
build(context)
val jetBrainsClientMainModule = context.productProperties.embeddedJetBrainsClientMainModule
@@ -203,7 +209,11 @@ private suspend fun doRunTestBuild(context: BuildContext, traceSpanName: String?
error = e
}
}
}
finally {
// close debug logging to prevent locking of the output directory on Windows
context.messages.close()
}
}
}
finally {
closeKtorClient()
@@ -213,16 +223,13 @@ private suspend fun doRunTestBuild(context: BuildContext, traceSpanName: String?
println("Performance report is written to $traceFile")
}
// close debug logging to prevent locking of output directory on Windows
context.messages.close()
/**
* Overridden in [org.jetbrains.intellij.build.impl.JpsCompilationRunner.runBuild]
*/
Logger.setFactory(defaultLogFactory)
try {
NioFiles.deleteRecursively(outDir)
outDir?.also(NioFiles::deleteRecursively)
}
catch (e: Throwable) {
System.err.println("cannot cleanup $outDir:")

View File

@@ -3,7 +3,6 @@ package org.jetbrains.intellij.build.pycharm
import com.intellij.openapi.application.PathManager
import com.intellij.platform.buildScripts.testFramework.runTestBuild
import com.intellij.platform.buildScripts.testFramework.spanName
import com.intellij.util.io.Compressor
import org.jetbrains.intellij.build.BuildOptions
import org.jetbrains.intellij.build.dependencies.BuildDependenciesCommunityRoot
@@ -42,7 +41,7 @@ class PyCharmCommunityBuildTest {
val communityHomePath = BuildDependenciesCommunityRoot(homePath.resolve("community"))
runTestBuild(
homeDir = communityHomePath.communityRoot,
traceSpanName = testInfo.spanName,
testInfo = testInfo,
productProperties = PyCharmCommunityProperties(communityHomePath.communityRoot),
) {
it.classOutDir = System.getProperty(BuildOptions.PROJECT_CLASSES_OUTPUT_DIRECTORY_PROPERTY)