diff --git a/MODULE.bazel b/MODULE.bazel index 5778ed649ad3..d23e6638eb34 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -17,6 +17,8 @@ local_path_override( path = "build/jvm-rules", ) +http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file") + bazel_dep(name = "hermetic_cc_toolchain", version = "4.0.1") toolchains = use_extension("@hermetic_cc_toolchain//toolchain:ext.bzl", "toolchains") @@ -88,6 +90,13 @@ use_repo( "kotlin_test_deps_parcelize-compiler-plugin-for-ide", ) +http_file( + name = "debugger_test_deps_debugger_agent", + url = "https://cache-redirector.jetbrains.com/packages.jetbrains.team/maven/p/ij/intellij-dependencies/org/jetbrains/intellij/deps/debugger-agent/1.79/debugger-agent-1.79.jar", + sha256 = "ba7a0aa7bbb813acab3b0edf36c0a15449f41896993c97e0d133fa4ea0a3e6d9", + downloaded_file_path = "debugger-agent.jar" +) + # GraalVM # git_override( # module_name = "rules_graalvm", diff --git a/java/debugger/impl/intellij.java.debugger.impl.iml b/java/debugger/impl/intellij.java.debugger.impl.iml index 1e6a4dbf0e15..2885de80fd4b 100644 --- a/java/debugger/impl/intellij.java.debugger.impl.iml +++ b/java/debugger/impl/intellij.java.debugger.impl.iml @@ -26,6 +26,8 @@ + + @@ -100,6 +102,7 @@ + diff --git a/java/debugger/impl/src/com/intellij/debugger/engine/AsyncStacksUtils.java b/java/debugger/impl/src/com/intellij/debugger/engine/AsyncStacksUtils.java index 50761f9e7c1d..dc209782f707 100644 --- a/java/debugger/impl/src/com/intellij/debugger/engine/AsyncStacksUtils.java +++ b/java/debugger/impl/src/com/intellij/debugger/engine/AsyncStacksUtils.java @@ -13,10 +13,12 @@ import com.intellij.debugger.memory.utils.StackFrameItem; import com.intellij.debugger.requests.ClassPrepareRequestor; import com.intellij.debugger.settings.CaptureSettingsProvider; import com.intellij.debugger.settings.DebuggerSettings; +import com.intellij.debugger.testFramework.TestDebuggerAgentArtifactsProvider; import com.intellij.debugger.ui.breakpoints.StackCapturingLineBreakpoint; import com.intellij.execution.JavaExecutionUtil; import com.intellij.execution.configurations.JavaParameters; import com.intellij.execution.configurations.ParametersList; +import com.intellij.ide.plugins.PluginManagerCore; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.AccessToken; import com.intellij.openapi.application.PathManager; @@ -353,6 +355,13 @@ public final class AsyncStacksUtils { } } + /** + * returns true if Bazel-test specific env variables are set + */ + private static Boolean isBazelTestRun() { + return System.getenv("TEST_SRCDIR") != null && System.getenv("TEST_UNDECLARED_OUTPUTS_DIR") != null; + } + @NativePath private static @Nullable String getAgentArtifactPath(@Nullable Project project, @Nullable Disposable disposable) { String relevantJarsRoot = PathManager.getArchivedCompliedClassesLocation(); @@ -375,13 +384,37 @@ public final class AsyncStacksUtils { try { Path agentArtifactPath = createTemporaryAgentPath(project, disposable); - Path communityRoot = Path.of(PathManager.getCommunityHomePath()); - Path iml = BuildDependenciesJps.getProjectModule(communityRoot, "intellij.java.debugger.agent.holder"); - Path downloadedAgent = BuildDependenciesJps.INSTANCE.getModuleLibrarySingleRootSync( - iml, - "debugger-agent", - "https://cache-redirector.jetbrains.com/intellij-dependencies", - new BuildDependenciesCommunityRoot(Path.of(PathManager.getCommunityHomePath()))); + Path downloadedAgent; + // In jps runs we resolve the debugger agent via the standard BuildDependenciesJps path below. + // However, when tests are executed under Bazel, they run inside a hermetic sandbox. In this environment: + // - Network/downloads and access to arbitrary files are restricted. + // - All runtime inputs must come from explicit Bazel-declared dependencies (runfiles). + // Therefore, in Bazel unit-test mode we must obtain the agent JAR from the test classpath via a test-only + // service. The TestDebuggerAgentArtifactsProvider is implemented in test sources and knows how to locate + // the agent artifact provided by Bazel via runfiles, so we use ServiceLoader to find that provider + // on the tests classpath and resolve the agent from there. Outside Bazel (regular production/dev runs), we + // keep using the standard resolution path that downloads the dependency if needed. + if (PluginManagerCore.isUnitTestMode && isBazelTestRun()) { + ServiceLoader providerClasses = ServiceLoader.load(TestDebuggerAgentArtifactsProvider.class); + var iterator = providerClasses.iterator(); + if (!iterator.hasNext()) { + throw new IllegalStateException("TestDebuggerAgentArtifactsProvider service provider not found"); + } + TestDebuggerAgentArtifactsProvider provider = iterator.next(); + if (iterator.hasNext()) { + throw new IllegalStateException("more than one TestDebuggerAgentArtifactsProvider service providers found. Only one is expected"); + } + + downloadedAgent = provider.getDebuggerAgentJar(); + } else { + Path communityRoot = Path.of(PathManager.getCommunityHomePath()); + Path iml = BuildDependenciesJps.getProjectModule(communityRoot, "intellij.java.debugger.agent.holder"); + downloadedAgent = BuildDependenciesJps.INSTANCE.getModuleLibrarySingleRootSync( + iml, + "debugger-agent", + "https://cache-redirector.jetbrains.com/intellij-dependencies", + new BuildDependenciesCommunityRoot(Path.of(PathManager.getCommunityHomePath()))); + } // The agent file must have a fixed name (AGENT_JAR_NAME) which is mentioned in MANIFEST.MF inside. // The copy operation is used as the rename operation. diff --git a/java/debugger/impl/src/com/intellij/debugger/testFramework/TestDebuggerAgentArtifactsProvider.java b/java/debugger/impl/src/com/intellij/debugger/testFramework/TestDebuggerAgentArtifactsProvider.java new file mode 100644 index 000000000000..44a333b124e1 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/testFramework/TestDebuggerAgentArtifactsProvider.java @@ -0,0 +1,21 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.debugger.testFramework; + +import java.nio.file.Path; + +/** + * Test-only service used to resolve debugger agent artifacts when running tests (especially under Bazel). + *

+ * In production and regular dev runs, the agent is resolved via standard mechanisms (e.g., BuildDependenciesJps). + * But Bazel tests execute in a hermetic sandbox where downloads and arbitrary file access are restricted and all + * inputs must be declared as dependencies (runfiles). To make the agent available in that environment, Bazel wires + * the artifact into the test's classpath/runfiles. This service, implemented in test sources, knows how to locate + * that artifact and provide its path to the runtime code. + *

+ * The implementation must be present on the tests classpath and is discovered via {@link java.util.ServiceLoader}. + */ +public interface TestDebuggerAgentArtifactsProvider { + + Path getDebuggerAgentJar(); + +} diff --git a/java/debugger/impl/test/com/intellij/debugger/artifacts/TestDebuggerAgentArtifactsProviderImpl.java b/java/debugger/impl/test/com/intellij/debugger/artifacts/TestDebuggerAgentArtifactsProviderImpl.java new file mode 100644 index 000000000000..bb0cef1171d3 --- /dev/null +++ b/java/debugger/impl/test/com/intellij/debugger/artifacts/TestDebuggerAgentArtifactsProviderImpl.java @@ -0,0 +1,23 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.debugger.artifacts; + +import com.intellij.debugger.testFramework.TestDebuggerAgentArtifactsProvider; +import com.intellij.testFramework.common.bazel.BazelLabel; +import com.intellij.testFramework.common.BazelTestUtil; + +import java.nio.file.Path; + +public class TestDebuggerAgentArtifactsProviderImpl implements TestDebuggerAgentArtifactsProvider { + + @Override + public Path getDebuggerAgentJar() { + String debuggerAgentLabel = "@debugger_test_deps_debugger_agent//file:debugger-agent.jar"; + BazelLabel label = BazelLabel.Companion.fromString(debuggerAgentLabel); + + if (BazelTestUtil.isUnderBazelTest()) { + return BazelTestUtil.getFileFromBazelRuntime(label); + } else { + throw new IllegalStateException("Expected to be used only in bazel-test environment"); + } + } +} diff --git a/java/debugger/impl/testResources/META-INF/services/com.intellij.debugger.testFramework.TestDebuggerAgentArtifactsProvider b/java/debugger/impl/testResources/META-INF/services/com.intellij.debugger.testFramework.TestDebuggerAgentArtifactsProvider new file mode 100644 index 000000000000..e7dab8b4db3c --- /dev/null +++ b/java/debugger/impl/testResources/META-INF/services/com.intellij.debugger.testFramework.TestDebuggerAgentArtifactsProvider @@ -0,0 +1 @@ +com.intellij.debugger.artifacts.TestDebuggerAgentArtifactsProviderImpl \ No newline at end of file diff --git a/platform/testFramework/common/src/common/BazelTestUtil.kt b/platform/testFramework/common/src/common/BazelTestUtil.kt index f74ac2a34c98..9aa50e515af4 100644 --- a/platform/testFramework/common/src/common/BazelTestUtil.kt +++ b/platform/testFramework/common/src/common/BazelTestUtil.kt @@ -1,10 +1,12 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.testFramework.common +import com.intellij.testFramework.common.bazel.BazelLabel import org.jetbrains.annotations.ApiStatus import java.nio.file.Path import kotlin.io.path.absolute import kotlin.io.path.isDirectory +import kotlin.io.path.isRegularFile import kotlin.io.path.useLines @ApiStatus.Experimental @@ -64,6 +66,22 @@ object BazelTestUtil { path } + @JvmStatic + fun getFileFromBazelRuntime(label: BazelLabel): Path { + val repoEntry = bazelTestRepoMapping.getOrElse(label.repo) { + error("Unable to determine dependency path '${label.asLabel}'") + } + val file = bazelTestRunfilesPath.resolve( + repoEntry.runfilesRelativePath + "/${label.packageName}/${label.file}" + ) + return when { + file.isRegularFile() || file.isDirectory() -> file.toAbsolutePath() + else -> { + error("Unable to find test dependency '${label.asLabel}' at $file") + } + } + } + /** * Tests under community root may run in community (OSS) or in the ultimate monorepo context. * diff --git a/platform/testFramework/common/src/common/bazel/BazelLabel.kt b/platform/testFramework/common/src/common/bazel/BazelLabel.kt new file mode 100644 index 000000000000..067a10c953f6 --- /dev/null +++ b/platform/testFramework/common/src/common/bazel/BazelLabel.kt @@ -0,0 +1,27 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.testFramework.common.bazel + +import org.jetbrains.annotations.ApiStatus + +// https://bazel.build/rules/lib/builtins/Label.html +@ApiStatus.Experimental +data class BazelLabel( + val repo: String, + val packageName: String, + val file: String, +) { + companion object { + private val regex = Regex("@([a-zA-Z0-9_-]+)//([a-z0-9_/-]+):([a-zA-Z0-9._-]+)") + fun fromString(label: String): BazelLabel { + val match = regex.matchEntire(label) ?: error("Bazel label must match '${regex.pattern}': $label") + return BazelLabel( + repo = match.groupValues[1], + packageName = match.groupValues[2], + file = match.groupValues[3], + ) + } + } + + val asLabel: String + get() = "@$repo//$packageName:$file" +} diff --git a/plugins/kotlin/base/plugin/test/org/jetbrains/kotlin/idea/artifacts/TestKotlinArtifacts.kt b/plugins/kotlin/base/plugin/test/org/jetbrains/kotlin/idea/artifacts/TestKotlinArtifacts.kt index 205361be2cf1..c02ce8112a0f 100644 --- a/plugins/kotlin/base/plugin/test/org/jetbrains/kotlin/idea/artifacts/TestKotlinArtifacts.kt +++ b/plugins/kotlin/base/plugin/test/org/jetbrains/kotlin/idea/artifacts/TestKotlinArtifacts.kt @@ -3,6 +3,7 @@ package org.jetbrains.kotlin.idea.base.plugin.artifacts import com.intellij.openapi.application.PathManager import com.intellij.testFramework.common.BazelTestUtil +import com.intellij.testFramework.common.bazel.BazelLabel import com.intellij.util.io.createParentDirectories import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking @@ -81,43 +82,6 @@ object TestKotlinArtifacts { listOf(getKotlinDepsByLabel("@kotlin_test_deps_kotlin-jps-plugin-classpath//file:kotlin-jps-plugin-classpath.jar").toPath()) } - // https://bazel.build/rules/lib/builtins/Label.html - data class BazelLabel( - val repo: String, - val packageName: String, - val file: String, - ) { - companion object { - private val regex = Regex("@([a-zA-Z0-9_-]+)//([a-z0-9_/-]+):([a-zA-Z0-9._-]+)") - fun fromString(label: String): BazelLabel { - val match = regex.matchEntire(label) ?: error("Bazel label must match '${regex.pattern}': $label") - return BazelLabel( - repo = match.groupValues[1], - packageName = match.groupValues[2], - file = match.groupValues[3], - ) - } - } - - val asLabel: String - get() = "@$repo//$packageName:$file" - } - - private fun getFileFromBazelRuntime(label: BazelLabel): Path { - val repoEntry = BazelTestUtil.bazelTestRepoMapping.getOrElse(label.repo) { - error("Unable to determine dependency path '${label.asLabel}'") - } - val file = BazelTestUtil.bazelTestRunfilesPath.resolve( - repoEntry.runfilesRelativePath + "/${label.packageName}/${label.file}" - ) - return when { - file.exists() -> file.toAbsolutePath() - else -> { - error("Unable to find test dependency '${label.asLabel}' at $file") - } - } - } - private data class HttpFile(val downloadFilePath: String, val name: String, val url: String) private val kotlinTestDependenciesHttpFiles by lazy { @@ -183,7 +147,7 @@ object TestKotlinArtifacts { private fun getKotlinDepsByLabel(label: BazelLabel): File { // Why it is different val dependency = if (BazelTestUtil.isUnderBazelTest) { - getFileFromBazelRuntime(label) + BazelTestUtil.getFileFromBazelRuntime(label) } else { downloadFile(label) } diff --git a/plugins/kotlin/jvm-debugger/test/k2/BUILD.bazel b/plugins/kotlin/jvm-debugger/test/k2/BUILD.bazel index d3216b0cad52..1c682a527366 100644 --- a/plugins/kotlin/jvm-debugger/test/k2/BUILD.bazel +++ b/plugins/kotlin/jvm-debugger/test/k2/BUILD.bazel @@ -5,12 +5,12 @@ jps_test( runtime_deps = [ ":k2_test_lib", "//:main_test_lib", - "@lib//:java-debugger-agent-holder-debugger-agent", ], data = [ "@kotlin_test_deps_org_jetbrains_kotlinx_kotlinx_coroutines_core_1_10_2//file:kotlinx_coroutines_core_1_10_2.jar", "@kotlin_test_deps_org_jetbrains_kotlinx_kotlinx_coroutines_core_jvm_1_10_2//file:kotlinx_coroutines_core_jvm_1_10_2.jar", + "@debugger_test_deps_debugger_agent//file:debugger-agent.jar", "@community//plugins/kotlin/jvm-debugger/test:testData", "@kotlin_test_deps_annotations//file:annotations.jar", "@kotlin_test_deps_junit//file:junit.jar", @@ -54,7 +54,7 @@ jvm_library( name = "k2_test_lib", module_name = "kotlin.jvm-debugger.test.k2", visibility = ["//visibility:public"], - srcs = glob(["test/**/*.kt", "test/**/*.java", "test/**/*.form"], allow_empty = True), + srcs = glob(["test/**/*.kt", "test/**/*.java"], allow_empty = True), deps = [ "//platform/core-ui", "//plugins/kotlin/plugin/k2",