[JEWEL-997] Write Maven artefacts validation script

This adds a new script, validate-maven-artifacts.main.kts, that ensures that the
Jewel artefacts publishing is set up correctly. The script will flag:
 * Differences in JewelMavenArtifacts across branches
 * Artefacts not being published in one or more branches
 * Artefact dependencies being different across branches

Usage: validate-maven-artifacts [<options>] <branches>...

This also fixes the usage help and readme for jps-bootstrap to mention that you
need to pass the project root (the ultimate or community root path) to the script.
There was no mention of it and I was having issues with the script, so after
checking with Nikolay Chashnikov and Leonid Shalupov, we agreed that should be updated.

closes https://github.com/JetBrains/intellij-community/pull/3209

GitOrigin-RevId: bfa5256d013442dcae392e0452f858c55951f3f6
This commit is contained in:
Sebastiano Poggi
2025-09-04 17:18:35 +00:00
committed by intellij-monorepo-bot
parent a687c95ac6
commit 4819a534e6
7 changed files with 709 additions and 29 deletions
+1 -1
View File
@@ -33,4 +33,4 @@ internal object JewelMavenArtifactsBuildTarget {
context.notifyArtifactBuilt(outputDir)
}
}
}
}
@@ -33,7 +33,7 @@ private data class CommitInfo(
val subject: String,
)
class CompareBranchesCommand : SuspendingCliktCommand() {
private class CompareBranchesCommand : SuspendingCliktCommand() {
private val verbose: Boolean by
option("--verbose", help = "Prints the normalized commit subjects being compared.").flag(default = false)
private val jewelOnly: Boolean by
@@ -207,12 +207,6 @@ class CompareBranchesCommand : SuspendingCliktCommand() {
}
}
private suspend fun isDirectoryGitRepo(directory: File): Boolean =
runCommand("git rev-parse --is-inside-work-tree", directory).isSuccess
private suspend fun branchExists(branch: String, directory: File): Boolean =
runCommand("git rev-parse --verify $branch", directory).isSuccess
private fun normalizeSubject(subject: String): String =
subject
.replaceFirst(Regex("""^(cherry picked from commit [0-9a-f]+):? """, RegexOption.IGNORE_CASE), "")
@@ -36,7 +36,7 @@ private object Config {
const val RELEASE_NOTES_FILE = "RELEASE NOTES.md"
}
class ExtractReleaseNotesCommand : CliktCommand() {
private class ExtractReleaseNotesCommand : CliktCommand() {
private val startDate: String by
option(
"--start-date",
@@ -105,7 +105,7 @@ class ExtractReleaseNotesCommand : CliktCommand() {
val elapsed = mark.elapsedNow()
println(" DONE")
printlnSuccess(" DONE")
println(" ℹ️ Found ${allCommitHashes.size} commits in $elapsed")
@@ -138,7 +138,7 @@ class ExtractReleaseNotesCommand : CliktCommand() {
val uniquePrCommits = prCommits.distinctBy { it.prId }.sortedBy { it.issueId }
println(" DONE")
printlnSuccess(" DONE")
println(
" ℹ️ Found ${uniquePrCommits.size} unique PRs to process. " +
@@ -4,9 +4,10 @@
@file:Import("utils.main.kts")
@file:Suppress("RAW_RUN_BLOCKING")
import kotlinx.coroutines.runBlocking
import com.github.pgreze.process.Redirect
import kotlin.system.exitProcess
import kotlin.time.Duration.Companion.minutes
import kotlinx.coroutines.runBlocking
private fun printHelp() {
println(
@@ -145,7 +146,7 @@ private val commands = buildList {
println("⏳ Executing: ${commands.joinToString(" ")}")
private val result = runBlocking {
runCommand(commands.joinToString(" "), jewelDir, inheritIO = true, timeoutAmount = 60.minutes)
runCommand(commands.joinToString(" "), jewelDir, timeoutAmount = 60.minutes, outputRedirect = Redirect.PRINT)
}
if (result.isSuccess) {
+64 -11
View File
@@ -1,10 +1,11 @@
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
@file:DependsOn("com.github.ajalt.clikt:clikt-jvm:5.0.3")
@file:DependsOn("com.github.pgreze:kotlin-process:1.5.1")
@file:Suppress("RAW_RUN_BLOCKING", "VariableNaming")
import com.github.ajalt.clikt.core.PrintMessage
import com.github.pgreze.process.Redirect
import com.github.pgreze.process.process
import kotlin.system.exitProcess
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.runBlocking
@@ -17,6 +18,7 @@ private val COMMUNITY_ROOT_MARKER_FILE_NAME = ".community.root.marker"
private val COMMUNITY_IML_FILE_NAME = "intellij.idea.community.main.iml"
private val CONTROL_CODE = '\u001b'
private val CODE_SUCCESS = "$CONTROL_CODE[0;32m"
private val CODE_ERR = "$CONTROL_CODE[0;31m"
private val CODE_WARN = "$CONTROL_CODE[0;33m"
private val CODE_BOLD = "$CONTROL_CODE[1m"
@@ -51,8 +53,7 @@ fun getPrNumber() = checkNotNull(System.getenv("PR_NUMBER")?.trim()) { "PR numbe
fun requireGhTool() {
if (checkGhTool()) return
printlnErr("ERROR: the GitHub CLI tool must be present on the PATH.")
exitProcess(1)
throw PrintMessage("ERROR: the GitHub CLI tool must be present on the PATH.", printError = true)
}
/**
@@ -102,8 +103,7 @@ private fun findDir(base: File, isDesiredDir: (File) -> Boolean): File? {
fun requirePrNumber() {
if (checkPrNumber()) return
printlnErr("ERROR: PR_NUMBER environment variable not set.")
exitProcess(2)
throw PrintMessage("ERROR: PR_NUMBER environment variable not set.", printError = true)
}
/**
@@ -124,6 +124,15 @@ fun printlnWarn(message: String) {
System.err.println(message.asWarning())
}
/**
* Prints a success message to the standard output stream with success (green) formatting.
*
* @param message The success message to print.
*/
fun printlnSuccess(message: String) {
println(message.asSuccess())
}
/**
* Formats a string as a warning message with ANSI escape codes.
*
@@ -145,6 +154,13 @@ fun String.asError() = if (stylingSupported) "$CODE_ERR$this$CODE_CLEAR" else th
*/
fun String.asBold(): String = if (stylingSupported) "$CODE_BOLD$this$CODE_CLEAR" else this
/**
* Formats a string as a success (green) text with ANSI escape codes if the terminal supports it.
*
* @return The formatted text, or the text itself if the terminal does not support styling.
*/
fun String.asSuccess(): String = if (stylingSupported) "$CODE_SUCCESS$this$CODE_CLEAR" else this
/**
* Detects if the current terminal likely supports OSC 8 hyperlinks by checking for known environment variables.
*
@@ -187,7 +203,7 @@ private val stylingSupported = doesTerminalSupportStyling()
* @return `true` if a compatible terminal is detected, `false` otherwise.
*/
private fun doesTerminalSupportStyling(): Boolean =
System.getenv("TERM")?.startsWith("xterm") ?: (System.getenv("COLORTERM") != null)
System.getenv("TERM")?.contains("color") == true || System.getenv("COLORTERM") == "truecolor"
/**
* Formats a string as a hyperlink using OSC 8 escape codes if the terminal supports it.
@@ -262,14 +278,14 @@ suspend fun runCommand(
workingDir: File?,
timeoutAmount: Duration = 60.seconds,
exitOnError: Boolean = true,
inheritIO: Boolean = false,
outputRedirect: Redirect = Redirect.CAPTURE,
): CmdResult {
val result =
withTimeout(timeoutAmount) {
process(
command = command.split(" ").toTypedArray(),
stdout = if (inheritIO) Redirect.PRINT else Redirect.CAPTURE,
stderr = if (inheritIO) Redirect.PRINT else Redirect.CAPTURE,
stdout = outputRedirect,
stderr = outputRedirect,
directory = workingDir,
)
}
@@ -279,8 +295,19 @@ suspend fun runCommand(
CmdResult.Success(output)
} else {
if (exitOnError) {
printlnErr("Command '$command' failed with exit code ${result.resultCode}:\n$output")
exitProcess(result.resultCode)
throw PrintMessage(
buildString {
appendLine()
append("Command '$command' failed with exit code ${result.resultCode}".asError())
if (output.isNotBlank()) {
appendLine(":".asError())
appendLine(output.asError())
} else {
appendLine()
}
},
printError = true,
)
}
CmdResult.Failure(output)
}
@@ -345,3 +372,29 @@ fun getLatestReleaseDate(baseDir: File = File(".").canonicalFile): String? {
printlnWarn("⚠️ Could not find any release date in RELEASE NOTES.md.")
return null
}
/**
* Checks whether the given directory is (part of) a git repository.
*
* @return true if it is, false otherwise.
*/
suspend fun isDirectoryGitRepo(directory: File): Boolean =
runCommand("git rev-parse --is-inside-work-tree", directory, exitOnError = false).isSuccess
/**
* Checks whether the given branch exists in the current git repository.
*
* @param branch The name of the branch to check.
* @return true if the branch exists, false otherwise.
*/
suspend fun branchExists(branch: String, directory: File): Boolean =
runCommand("git rev-parse --verify $branch", directory).isSuccess
val isWindows = "windows" in System.getProperty("os.name").lowercase()
fun exitWithError(message: String): Nothing {
throw PrintMessage(message.asError(), printError = true)
}
suspend fun getCurrentBranchName(directory: File): String =
runCommand("git rev-parse --abbrev-ref HEAD", directory, exitOnError = true).output.trim()
+624
View File
@@ -0,0 +1,624 @@
#!/usr/bin/env kotlin
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:Suppress("RAW_RUN_BLOCKING", "TooGenericExceptionCaught")
@file:DependsOn("com.github.ajalt.clikt:clikt-jvm:5.0.3")
@file:Import("utils.main.kts")
import com.github.ajalt.clikt.command.SuspendingCliktCommand
import com.github.ajalt.clikt.command.main
import com.github.ajalt.clikt.core.Context
import com.github.ajalt.clikt.core.terminal
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.multiple
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.file
import com.github.ajalt.mordant.terminal.YesNoPrompt
import com.github.pgreze.process.Redirect
import java.io.File
import java.nio.file.Files
import javax.xml.parsers.DocumentBuilderFactory
import kotlin.io.path.ExperimentalPathApi
import kotlin.time.Duration.Companion.minutes
import kotlinx.coroutines.runBlocking
import org.w3c.dom.Element
import org.w3c.dom.Node
import org.w3c.dom.NodeList
@OptIn(ExperimentalPathApi::class)
private class ValidateMavenArtifactsCommand : SuspendingCliktCommand() {
private val branches: List<String> by
argument(help = "Branches to build and compare artifacts from (at least two).").multiple(required = true)
private val forcePull: Boolean by
option("--force-pull", help = "Force pull untracked branches before building.").flag(default = false)
private val verbose: Boolean by option("--verbose", "-v", help = "Enable verbose output.").flag(default = false)
private val artifactsDir: File? by
option(
"--artifacts-dir",
"-a",
"--out-dir",
"-o",
help =
"Directory to store the built artifacts. " +
"If not specified, it will create a temp dir in the system temp dir.",
)
.file(mustExist = false, canBeFile = false)
private val preserveTemp: Boolean by
option(
"--preserve-temp",
"-p",
help =
"Do not clean up the temporary directory after execution. Implicit when --no-build is specified.",
)
.flag(default = false)
private val noBuild: Boolean by
option(
"--no-build",
"-n",
help =
"Skip building artifacts, only validate the artifacts in --artifacts-dir. " +
"Requires passing --artifacts-dir, implies --preserve-temp.",
)
.flag(default = false)
private val mavenArtifactsOutputDirName = "maven-artifacts" // Matches JewelMavenArtifactsBuildTarget
override fun help(context: Context): String = "Validates Jewel Maven artifacts across specified branches."
override suspend fun run() {
if (branches.size < 2) {
exitWithError("Please specify at least two branches to compare.")
}
print("⏳ Locating IntelliJ Community root...")
val communityRoot = findCommunityRoot()
if (communityRoot == null || !communityRoot.isDirectory) {
println()
exitWithError(
"Could not find the IntelliJ Community root directory. " +
"Please make sure you're running the script from somewhere inside it."
)
}
println(" DONE: ${communityRoot.canonicalPath}")
print("⏳ Checking git status...")
if (!isDirectoryGitRepo(communityRoot)) {
println()
exitWithError("Not a git repository at ${communityRoot.canonicalPath}.")
}
// Check if the repo has pending changes, which can cause issues when switching branches.
// If --no-build is specified, we don't switch branches to build, so this is not needed.
if (!noBuild) {
val gitStatus = runCommand("git status --porcelain", communityRoot, exitOnError = true).output.trim()
if (gitStatus.isNotEmpty()) {
println()
exitWithError(
"Local changes detected in the git repository at ${communityRoot.canonicalPath}. " +
"Please commit or stash them."
)
}
}
// Store the original branch to return to later
val originalBranch = getCurrentBranchName(communityRoot)
printlnSuccess(" DONE")
if (verbose) println(" Original branch: $originalBranch")
val mavenArtifactsDir = communityRoot.resolve("out/idea-ce/artifacts/$mavenArtifactsOutputDirName")
val artifactsDirectory = artifactsDir ?: Files.createTempDirectory("jewel-artifacts").toFile()
if (noBuild) {
if (artifactsDirectory == null || !artifactsDirectory.exists() || !artifactsDirectory.isDirectory) {
exitWithError(
"When --no-build is specified, --artifacts-dir must be " +
"provided and point to an existing directory."
)
}
println("ℹ️ Skipping build and validating from ${artifactsDirectory.canonicalPath}")
val hasDiscrepancies = validateMavenArtifacts(artifactsDirectory)
if (!hasDiscrepancies) {
println("✅ No discrepancies found in artifact presence or dependencies.")
} else {
exitWithError("❌ Discrepancies found. Please review the output.")
}
} else {
validateAndBuildArtifacts(mavenArtifactsDir, artifactsDirectory, communityRoot, originalBranch)
if (!preserveTemp && artifactsDirectory.exists() && artifactsDir == null) {
println("🧹 Cleaning temporary artifacts directory: ${artifactsDirectory.canonicalPath}")
artifactsDirectory.deleteRecursively()
}
}
}
private suspend fun validateAndBuildArtifacts(
mavenArtifactsDir: File,
artifactsRoot: File,
communityRoot: File,
originalBranch: String,
) {
try {
checkJewelMavenArtifactsFileConsistency(communityRoot)
print("🧹 Cleaning output directory: $mavenArtifactsDir...")
if (mavenArtifactsDir.exists()) {
mavenArtifactsDir.deleteRecursively()
}
mavenArtifactsDir.mkdirs()
printlnSuccess(" DONE")
if (artifactsRoot.exists()) {
print("🧹 Cleaning artifacts directory: $artifactsRoot...")
artifactsRoot.deleteRecursively()
printlnSuccess(" DONE")
}
artifactsRoot.mkdirs()
for (branch in branches) {
println("\n--- Building artifacts for branch ${branch.asBold()} ---")
checkoutBranch(branch, communityRoot)
val buildTargetFile = communityRoot.resolve("build/src/JewelMavenArtifactsBuildTarget.kt")
if (!buildTargetFile.isFile) {
exitWithError("Build target file not found at ${buildTargetFile.canonicalPath}")
}
println("🩹 Patching ${buildTargetFile.relativeTo(communityRoot).toString().asBold()}")
val patchedBuildTargetContent =
buildTargetFile
.readText()
.replace(
"projectHome = ULTIMATE_HOME",
"projectHome = COMMUNITY_ROOT.communityRoot",
)
buildTargetFile.writeText(patchedBuildTargetContent)
pullBranchIfNeeded(branch, communityRoot)
buildArtifactsOnBranch(communityRoot, branch)
// Copy artifacts to the temp directory
println("📦 Copying artifacts for branch ${branch.asBold()} to ${artifactsRoot.canonicalPath}")
mavenArtifactsDir.copyRecursively(artifactsRoot, true)
}
println()
val hasDiscrepancies = validateMavenArtifacts(artifactsRoot)
if (!hasDiscrepancies) {
println("✅ No discrepancies found in artifact presence or dependencies.")
} else {
exitWithError("❌ Discrepancies found. Please review the output.")
}
} finally {
cleanup(communityRoot, originalBranch, artifactsRoot)
}
}
private suspend fun checkJewelMavenArtifactsFileConsistency(communityRoot: File) {
println("\n🔍 Verifying JewelMavenArtifacts.kt consistency across branches...")
val filePath = "build/src/org/jetbrains/intellij/build/JewelMavenArtifacts.kt"
val contentsByBranch =
branches.associateWith { branch ->
println(" Checking branch $branch...")
runCommand("git show $branch:$filePath", communityRoot, exitOnError = false)
}
val failedBranches = contentsByBranch.filterValues { it.isFailure }.map { it.key }
if (failedBranches.isNotEmpty()) {
exitWithError("Could not read $filePath from branches: ${failedBranches.joinToString()}.")
}
val contents = contentsByBranch.mapValues { it.value.getOrThrow() }
val referenceBranch = branches.first()
val referenceContent = contents[referenceBranch]
val differingBranches = branches.drop(1).filter { contents[it] != referenceContent }
if (differingBranches.isEmpty()) {
printlnSuccess("✅ JewelMavenArtifacts.kt is consistent across all branches.")
return
}
printlnWarn(
"Found differences in build/src/org/jetbrains/intellij/build/JewelMavenArtifacts.kt between branches."
)
printlnWarn("This is likely to produce inconsistent build results.")
for (branch in differingBranches) {
println("\n--- Diff between ${referenceBranch.asBold()} and ${branch.asBold()} ---")
val diffResult =
runCommand("git diff $referenceBranch $branch -- $filePath", communityRoot, exitOnError = false)
if (diffResult.isSuccess && diffResult.output.isNotBlank()) {
println(diffResult.output)
} else {
printlnErr("Could not compute diff for $filePath between branches '$referenceBranch' and '$branch'.")
}
}
if (YesNoPrompt("Do you want to proceed with the build anyway?", terminal, default = true).ask() == false) {
exitWithError("Build cancelled by user due to file inconsistencies.")
}
}
private suspend fun checkoutBranch(branch: String, communityRoot: File) {
print("Checking out branch '$branch'...")
val checkoutResult = runCommand("git checkout $branch", communityRoot)
if (checkoutResult.isFailure) {
// Attempt to stash and retry checkout
printlnWarn("Checkout failed, attempting to stash changes and retry...")
runCommand("git stash", communityRoot, exitOnError = false)
val retryCheckoutResult = runCommand("git checkout $branch", communityRoot)
if (retryCheckoutResult.isFailure) {
println()
exitWithError("Failed to checkout branch '$branch' even after stashing. Aborting.")
} else {
println(" Checkout successful after stashing.")
}
} else {
printlnSuccess(" DONE")
}
}
private suspend fun pullBranchIfNeeded(branch: String, communityRoot: File) {
val remoteInfo =
runCommand(
command = "git for-each-ref --format='%(upstream:remotename)' refs/heads/$branch",
workingDir = communityRoot,
exitOnError = false,
)
.output
.trim()
val trackingBranch =
runCommand(
command = "git for-each-ref --format='%(upstream:short)' refs/heads/$branch",
workingDir = communityRoot,
exitOnError = false,
)
.output
.trim()
if (remoteInfo.isNotEmpty() && trackingBranch.isNotEmpty()) {
val pullNeeded =
runCommand("git status -uno", communityRoot, exitOnError = false).output.let {
it.contains("Your branch is behind") || it.contains("Your branch and") // Covers divergence
}
if (pullNeeded || forcePull) {
pullBranch(branch, trackingBranch, communityRoot)
} else if (verbose) {
println("Branch '$branch' is up-to-date or doesn't need pulling.")
}
} else if (verbose) {
println("Branch '$branch' does not track a remote branch.")
}
}
private suspend fun pullBranch(branch: String, trackingBranch: String, communityRoot: File) {
val shouldPull =
if (forcePull) {
true
} else {
print("Branch '$branch' tracks '$trackingBranch'. Do you want to pull? (y/n): ")
readlnOrNull()?.trim()?.lowercase() == "y"
}
if (shouldPull) {
println("Pulling from '$trackingBranch'...")
val pullResult =
runCommand(
"git pull --rebase",
communityRoot,
exitOnError = false,
)
if (pullResult.isFailure) {
val statusAfterPull = runCommand("git status", communityRoot).output
if (
statusAfterPull.contains("Unmerged paths") || statusAfterPull.contains("You have unstaged changes")
) {
printlnErr("Conflicts detected after pull. Please resolve manually and re-run the script.")
}
exitWithError("Failed to pull branch '$branch'. Aborting...")
}
println("Pull successful.")
} else {
println("Skipping pull for branch '$branch'.")
}
}
private suspend fun buildArtifactsOnBranch(communityRoot: File, branch: String) {
println("🚀 Building artifacts...")
val buildCommand = buildString {
append("platform/jps-bootstrap/jps-bootstrap")
append(if (isWindows) ".cmd" else ".sh")
append(" ")
append(communityRoot.absolutePath)
append(" intellij.idea.community.build JewelMavenArtifactsBuildTarget")
}
if (verbose) println("Build command: $buildCommand")
val buildResult =
runCommand(
buildCommand,
communityRoot,
timeoutAmount = 30.minutes, // Allow more time for build
outputRedirect = if (verbose) Redirect.PRINT else Redirect.CAPTURE,
)
if (buildResult.isFailure) {
println(buildResult.output)
exitWithError("Build failed for branch '$branch'. Aborting...")
}
println("Build successful for branch ${branch.asBold()}.")
}
private fun validateMavenArtifacts(artifactsDir: File): Boolean {
println("\n--- Comparing built artifacts ---")
val pomFiles =
artifactsDir
.walkTopDown()
.onFail { file, ioException -> printlnErr("Error accessing $file: $ioException") }
.filter { it.extension.lowercase() == "pom" }
.toList()
if (verbose) {
println("Found ${pomFiles.size} POM files. Computing distinct groupId:artifactId coordinates...")
}
if (pomFiles.isEmpty()) {
exitWithError("No POM files found in the temporary output directory.")
}
val artifactNames = pomFiles.map { getArtifactNameFromPom(it) }.toSortedSet()
println("Found ${artifactNames.size} unique artifacts.")
if (verbose) {
println("Artifact coordinates:")
artifactNames.sorted().forEach { println(" - $it") }
}
return checkForDiscrepancies(artifactNames, pomFiles)
}
private fun checkForDiscrepancies(
allFoundArtifactNames: MutableSet<String>,
pomFiles: List<File>,
): Boolean {
println("\nVerifying artifact presence across branches...")
var hasDiscrepancies = false
// First, check that we have all versions for each artifact
for (artifactName in allFoundArtifactNames) {
val pomsForArtifact = pomFiles.filter { pomFile -> getArtifactNameFromPom(pomFile) == artifactName }
if (pomsForArtifact.size != branches.size) {
printlnErr(
buildString {
append(" * Artifact ")
append(artifactName.asBold())
append(" is missing in one or more branches.".asError())
append(" (")
append(branches.size)
append(" expected, ")
append(pomsForArtifact.size)
append(" found).")
}
)
printlnErr(" Found versions:")
pomsForArtifact.forEach { println(" - " + getVersionFromPom(it).asBold()) }
hasDiscrepancies = true
}
}
// Then, compare dependencies for each artifact across versions
return checkForDependencyDiscrepancies(
allFoundArtifactNames,
pomFiles,
) || hasDiscrepancies
}
private fun checkForDependencyDiscrepancies(
artifactNames: MutableSet<String>,
pomFiles: List<File>,
): Boolean {
println("\nVerifying dependencies across branches...")
var hasDiscrepancies = false
for (artifactName in artifactNames) {
val pomsForArtifact = pomFiles.filter { pomFile -> getArtifactNameFromPom(pomFile) == artifactName }
if (pomsForArtifact.size < 2) {
printlnWarn(
"Skipping dependency comparison for ${artifactName.asBold()}: " +
"only ${pomsForArtifact.size} POMs found."
)
continue
}
val dependencySets = pomsForArtifact.associateWith { pomFile -> getDependenciesFromPom(pomFile) }
hasDiscrepancies =
crossValidateDependencies(pomsForArtifact, dependencySets, artifactName) || hasDiscrepancies
}
return hasDiscrepancies
}
private fun getArtifactNameFromPom(pomFile: File): String =
try {
val factory = DocumentBuilderFactory.newInstance()
val builder = factory.newDocumentBuilder()
val doc = builder.parse(pomFile)
doc.documentElement.normalize()
val groupId =
checkNotNull(doc.getElementsByTagName("groupId").item(0).textContent) { "groupId not found" }.trim()
val artifactId =
checkNotNull(doc.getElementsByTagName("artifactId").item(0).textContent) { "artifactId not found" }
.trim()
"$groupId:$artifactId"
} catch (e: Exception) {
exitWithError("Error parsing POM file ${pomFile.name}: ${e.message}")
}
private fun getDependenciesFromPom(pomFile: File): Set<String> {
return try {
val factory = DocumentBuilderFactory.newInstance()
val builder = factory.newDocumentBuilder()
val doc = builder.parse(pomFile)
doc.documentElement.normalize()
val dependencies = mutableSetOf<String>()
val dependencyNodes: NodeList = doc.getElementsByTagName("dependency")
for (i in 0 until dependencyNodes.length) {
val dependencyNode: Node = dependencyNodes.item(i)
val coordinates = processDependencyNode(dependencyNode, pomFile.name, i)
if (coordinates != null) dependencies.add(coordinates)
}
dependencies
} catch (e: Exception) {
printlnErr("Error parsing dependencies in POM file ${pomFile.name}: ${e.message}")
emptySet()
}
}
private fun processDependencyNode(dependencyNode: Node, fileName: String, position: Int): String? {
if (dependencyNode.nodeType != Node.ELEMENT_NODE) return null
val element = dependencyNode as Element
val groupId = element.getElementsByTagName("groupId").item(0)?.textContent?.trim()
val artifactId = element.getElementsByTagName("artifactId").item(0)?.textContent?.trim()
return if (groupId != null && artifactId != null) {
"$groupId:$artifactId"
} else {
printlnWarn("Skipping incomplete dependency in $fileName at position #$position")
null
}
}
private fun crossValidateDependencies(
poms: List<File>,
dependencySets: Map<File, Set<String>>,
artifactName: String,
): Boolean {
var hasDiscrepancies = false
for (i in 0 until poms.size) {
for (j in i + 1 until poms.size) {
val file1 = poms[i]
val file2 = poms[j]
val deps1 = dependencySets[file1] ?: emptySet()
val deps2 = dependencySets[file2] ?: emptySet()
hasDiscrepancies =
crossValidateDependencies(artifactName, file1, file2, deps1, deps2) || hasDiscrepancies
}
}
println()
return hasDiscrepancies
}
private fun crossValidateDependencies(
artifactName: String,
file1: File,
file2: File,
deps1: Set<String>,
deps2: Set<String>,
): Boolean {
var hasDiscrepancies = false
val discrepancies = (deps1 union deps2) - (deps1 intersect deps2)
if (discrepancies.isNotEmpty()) {
printlnErr(
buildString {
append(" * Dependencies DIFFER for ")
append(artifactName.asBold())
append(" between ")
append(file1.name.asBold().asLink(file1.absolutePath))
append(" and ")
append(file2.name.asBold().asLink(file2.absolutePath))
append(".")
}
)
println(" Unique dependencies:")
discrepancies.sorted().forEach { println(" - ${it.asBold()}") }
hasDiscrepancies = true
} else {
printlnSuccess(
buildString {
append(" * Dependencies MATCH for ")
append(artifactName.asBold())
append(" between ")
append(file1.name.asBold().asLink(file1.absolutePath))
append(" and ")
append(file2.name.asBold().asLink(file2.absolutePath))
append(".")
}
)
}
return hasDiscrepancies
}
private suspend fun cleanup(
communityRoot: File,
originalBranch: String,
artifactsRoot: File,
) {
println("\n--- Cleaning up ---")
print("🔄 Reverting local changes...")
runCommand("git reset --hard", communityRoot)
printlnSuccess(" DONE")
print("🔄 Returning to original branch '$originalBranch'...")
runCommand("git checkout $originalBranch", communityRoot)
printlnSuccess(" DONE")
if (!preserveTemp) {
println("🧹 Cleaning artifacts directory: ${artifactsRoot.canonicalPath}")
if (artifactsRoot.exists()) {
artifactsRoot.deleteRecursively()
}
} else {
println("ℹ️ Skipping cleanup of artifacts directory: ${artifactsRoot.canonicalPath}")
}
}
private fun getVersionFromPom(pomFile: File): String =
try {
val factory = DocumentBuilderFactory.newInstance()
val builder = factory.newDocumentBuilder()
val doc = builder.parse(pomFile)
doc.documentElement.normalize()
checkNotNull(doc.getElementsByTagName("version").item(0)?.textContent?.trim()) { "version not found" }
} catch (e: Exception) {
exitWithError("Error parsing POM file ${pomFile.name}: ${e.message}")
}
}
runBlocking { ValidateMavenArtifactsCommand().main(args) }
+13 -5
View File
@@ -20,11 +20,19 @@ On buildserver it's not easy to output something correctly, the output is interc
## Running build scripts via jps-bootstrap
### Locally
* just run CLASS_NAME from IDEA
* run any main class (written in Java/Kotlin) from intellij project via\
`./jps-bootstrap.cmd PROJECT_HOME MODULE_NAME CLASS_NAME ARGS`
Example: `./community/platform/jps-bootstrap/jps-bootstrap.sh ./community intellij.idea.ultimate.build DownloadLibrariesBuildTarget`
* Run the CLASS_NAME from IDEA from the gutter ▶️ icon, or
* Run any main class (written in Java/Kotlin) from the IntelliJ project from CLI via:
```shell
./jps-bootstrap.cmd PROJECT_HOME MODULE_NAME CLASS_NAME ARGS
```
Where `PROJECT_HOME` is the path where the `.idea` folder is (either the ultimate or community root), the `MODULE_NAME` is the name of
the JPS module that contains the class, and `CLASS_NAME` is the FQN of the class that contains the `main()` to run.
Example:
```shell
./community/platform/jps-bootstrap/jps-bootstrap.sh . intellij.idea.ultimate.build DownloadLibrariesBuildTarget
```
Special wrappers could be written to make scripts easier, see e.g. `build/downloadLibraries.cmd`