diff --git a/plugins/maven/maven-event-listener/src/main/java/org/jetbrains/maven/server/IntellijMavenSpy.java b/plugins/maven/maven-event-listener/src/main/java/org/jetbrains/maven/server/IntellijMavenSpy.java index 6ae41dd76828..513e449e1873 100644 --- a/plugins/maven/maven-event-listener/src/main/java/org/jetbrains/maven/server/IntellijMavenSpy.java +++ b/plugins/maven/maven-event-listener/src/main/java/org/jetbrains/maven/server/IntellijMavenSpy.java @@ -22,26 +22,11 @@ import static org.jetbrains.maven.server.SpyConstants.NEWLINE; @Named("Intellij Idea Maven Embedded Event Spy") @Singleton public class IntellijMavenSpy extends AbstractEventSpy { - private EventInfoPrinter myPrinter; + private static EventInfoPrinter ourPrinter = new EventInfoPrinter(s -> System.out.println(s)); - @SuppressWarnings("UseOfSystemOutOrSystemErr") - @Override - public void init(Context context) throws Exception { - try { - doInit(context); - } - catch (Throwable ignore) { - } - if (myPrinter == null) { - myPrinter = new EventInfoPrinter(s -> System.out.println(s)); - System.err.println("IntellijMavenSpy not initialized, build nodes could be displayed incorrectly"); - } - } - - private void doInit(Context context) - throws Throwable { - myPrinter = new EventInfoPrinter(s -> System.out.println(s)); + public static void printInternalLogging(String message){ + ourPrinter.printMavenEventInfo("IDEA_INTERNAL_MESSAGE", "error", message); } @Override @@ -67,7 +52,7 @@ public class IntellijMavenSpy extends AbstractEventSpy { private void onDependencyResolutionRequest(DependencyResolutionRequest event) { String projectId = event.getMavenProject() == null ? "unknown" : event.getMavenProject().getId(); - myPrinter.printMavenEventInfo("DependencyResolutionRequest", "id", projectId); + ourPrinter.printMavenEventInfo("DependencyResolutionRequest", "id", projectId); } private void onDependencyResolutionResult(DependencyResolutionResult event) { @@ -79,7 +64,7 @@ public class IntellijMavenSpy extends AbstractEventSpy { } result.append(e.getMessage()); } - myPrinter.printMavenEventInfo("DependencyResolutionResult", "error", result); + ourPrinter.printMavenEventInfo("DependencyResolutionResult", "error", result); } private void collectAndPrintLastLinesForEA(Throwable e) { @@ -90,7 +75,7 @@ public class IntellijMavenSpy extends AbstractEventSpy { for (int i = 0; i < lines; i++) { builder.append(e.getStackTrace()[i]).append("\n"); } - myPrinter.printMavenEventInfo("INTERR", "error", builder); + ourPrinter.printMavenEventInfo("INTERR", "error", builder); } private void onRepositoryEvent(RepositoryEvent event) { @@ -103,7 +88,7 @@ public class IntellijMavenSpy extends AbstractEventSpy { String errMessage = event.getException() == null ? "" : event.getException().getMessage(); String path = event.getFile() == null ? "" : event.getFile().getPath(); String artifactCoord = event.getArtifact() == null ? "" : event.getArtifact().toString(); - myPrinter.printMavenEventInfo(type, "path", path, "artifactCoord", artifactCoord, "error", errMessage); + ourPrinter.printMavenEventInfo(type, "path", path, "artifactCoord", artifactCoord, "error", errMessage); } private void onExecutionEvent(ExecutionEvent event) { @@ -111,17 +96,17 @@ public class IntellijMavenSpy extends AbstractEventSpy { String projectId = event.getProject() == null ? "unknown" : event.getProject().getId(); if (mojoExec != null) { String errMessage = event.getException() == null ? "" : getErrorMessage(event.getException()); - myPrinter.printMavenEventInfo(event.getType(), "source", String.valueOf(mojoExec.getSource()), "goal", mojoExec.getGoal(), "id", - projectId, - "error", - errMessage); + ourPrinter.printMavenEventInfo(event.getType(), "source", String.valueOf(mojoExec.getSource()), "goal", mojoExec.getGoal(), "id", + projectId, + "error", + errMessage); } else { if (event.getType() == ExecutionEvent.Type.SessionStarted) { printSessionStartedEventAndReactorData(event, projectId); } else { - myPrinter.printMavenEventInfo(event.getType(), "id", projectId); + ourPrinter.printMavenEventInfo(event.getType(), "id", projectId); } } } @@ -147,10 +132,10 @@ public class IntellijMavenSpy extends AbstractEventSpy { .append(project.getArtifactId()).append(":") .append(project.getVersion()).append("&&"); } - myPrinter.printMavenEventInfo(ExecutionEvent.Type.SessionStarted, "id", projectId, "projects", builder); + ourPrinter.printMavenEventInfo(ExecutionEvent.Type.SessionStarted, "id", projectId, "projects", builder); } else { - myPrinter.printMavenEventInfo(ExecutionEvent.Type.SessionStarted, "id", projectId, "projects", ""); + ourPrinter.printMavenEventInfo(ExecutionEvent.Type.SessionStarted, "id", projectId, "projects", ""); } } } diff --git a/plugins/maven/maven-event-listener/src/main/java/org/jetbrains/maven/server/workspace/IntelliJWorkspaceReader.java b/plugins/maven/maven-event-listener/src/main/java/org/jetbrains/maven/server/workspace/IntelliJWorkspaceReader.java new file mode 100644 index 000000000000..628d4d08983f --- /dev/null +++ b/plugins/maven/maven-event-listener/src/main/java/org/jetbrains/maven/server/workspace/IntelliJWorkspaceReader.java @@ -0,0 +1,47 @@ +// 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.maven.server.workspace; + +import org.codehaus.plexus.component.annotations.Component; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.repository.WorkspaceReader; +import org.eclipse.aether.repository.WorkspaceRepository; + +import java.io.File; +import java.util.Collections; +import java.util.List; + +@Component(role = WorkspaceReader.class, hint = "ide") +public class IntelliJWorkspaceReader implements WorkspaceReader { + + private final WorkspaceRepository myWorkspaceRepository; + + public IntelliJWorkspaceReader() { + myWorkspaceRepository = new WorkspaceRepository("ide", getClass()); + } + + @Override + public int hashCode() { + return 42; + } + + @Override + public boolean equals(Object o) { + return o instanceof IntelliJWorkspaceReader; + } + + @Override + public WorkspaceRepository getRepository() { + return myWorkspaceRepository; + } + + @SuppressWarnings("IO_FILE_USAGE") + @Override + public File findArtifact(Artifact artifact) { + return MavenModuleMap.getInstance().findArtifact(artifact); + } + + @Override + public List findVersions(Artifact artifact) { + return Collections.emptyList(); + } +} diff --git a/plugins/maven/maven-event-listener/src/main/java/org/jetbrains/maven/server/workspace/MavenModuleMap.java b/plugins/maven/maven-event-listener/src/main/java/org/jetbrains/maven/server/workspace/MavenModuleMap.java new file mode 100644 index 000000000000..154d45a81b11 --- /dev/null +++ b/plugins/maven/maven-event-listener/src/main/java/org/jetbrains/maven/server/workspace/MavenModuleMap.java @@ -0,0 +1,78 @@ +// 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.maven.server.workspace; + +import org.eclipse.aether.artifact.Artifact; +import org.jetbrains.maven.server.IntellijMavenSpy; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Properties; + +public final class MavenModuleMap { + + private static final MavenModuleMap ourInstance = new MavenModuleMap(); + + public static final String PATHS_FILE_PROPERTY = "idea.modules.paths.file"; + + private final Properties myMap = new Properties(); + + private MavenModuleMap() { + String path = System.getProperty(PATHS_FILE_PROPERTY); + if (path != null) { + IntellijMavenSpy.printInternalLogging("reading idea.modules.paths from " + path); + try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(Paths.get(path)))) { + myMap.load(in); + IntellijMavenSpy.printInternalLogging("have read " + myMap.size() + " records"); + } + catch (IOException e) { + IntellijMavenSpy.printInternalLogging(e.getMessage()); + } + } + else { + IntellijMavenSpy.printInternalLogging("idea.modules.paths.file is not defined"); + } + } + + public static MavenModuleMap getInstance() { + return ourInstance; + } + + @SuppressWarnings("IO_FILE_USAGE") + public File findArtifact(Artifact artifact) { + String key = getKey(artifact); + String value = myMap.getProperty(key); + if (value == null || value.isEmpty()) { + return null; + } + + File file = new File(value); + if (!file.exists()) { + return null; + } + + return file; + } + + private static String getKey(Artifact artifact) { + String type = getType(artifact); + return artifact.getGroupId() + ':' + artifact.getArtifactId() + ':' + type + ':' + artifact.getBaseVersion(); + } + + private static String getType(Artifact artifact) { + String extension = artifact.getExtension(); + String classifier = artifact.getClassifier(); + + if ("jar".equals(extension) && classifier != null && !classifier.isEmpty()) { + if ("tests".equals(classifier) || "test-jar".equals(classifier)) { + return "test-jar"; + } + return classifier; + } + else { + return extension; + } + } +} diff --git a/plugins/maven/maven-event-listener/src/main/resources/META-INF/plexus/components.xml b/plugins/maven/maven-event-listener/src/main/resources/META-INF/plexus/components.xml new file mode 100644 index 000000000000..6ae224d898d8 --- /dev/null +++ b/plugins/maven/maven-event-listener/src/main/resources/META-INF/plexus/components.xml @@ -0,0 +1,12 @@ + + + + + org.eclipse.aether.repository.WorkspaceReader + ide + org.jetbrains.maven.server.workspace.IntelliJWorkspaceReader + + false + + + diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/execution/MavenExternalParameters.java b/plugins/maven/src/main/java/org/jetbrains/idea/maven/execution/MavenExternalParameters.java index 76393534c9c3..4c5b95948578 100644 --- a/plugins/maven/src/main/java/org/jetbrains/idea/maven/execution/MavenExternalParameters.java +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/execution/MavenExternalParameters.java @@ -69,7 +69,9 @@ public final class MavenExternalParameters { /** * @param runConfiguration used to creation fix if maven home not found + * @deprecated use maven script running instead */ + @Deprecated public static JavaParameters createJavaParameters(final @NotNull Project project, final @NotNull MavenRunnerParameters parameters, @Nullable MavenGeneralSettings coreSettings, @@ -140,7 +142,7 @@ public final class MavenExternalParameters { if (parameters.isResolveToWorkspace()) { try { - String resolverJar = getArtifactResolverJar(mavenVersion); + String resolverJar = getArtifactResolverJar(); confFile = patchConfFile(confFile, resolverJar); File modulesPathsFile = dumpModulesPaths(project); @@ -212,7 +214,7 @@ public final class MavenExternalParameters { return tmpConf; } - private static void patchConfFile(Path originalConf, Path dest, String library) throws IOException { + public static void patchConfFile(Path originalConf, Path dest, String library) throws IOException { try (Scanner sc = new Scanner(originalConf, StandardCharsets.UTF_8); BufferedWriter out = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(dest), StandardCharsets.UTF_8))) { @@ -234,7 +236,8 @@ public final class MavenExternalParameters { } } - private static String getArtifactResolverJar(@Nullable String mavenVersion) throws IOException { + + public static String getArtifactResolverJar() throws IOException { Class marker = MavenArtifactResolvedM31RtMarker.class; File classDirOrJar = new File(PathUtil.getJarPathForClass(marker)); @@ -257,42 +260,10 @@ public final class MavenExternalParameters { return tempFile.getAbsolutePath(); } - private static File dumpModulesPaths(@NotNull Project project) throws IOException { + public static File dumpModulesPaths(@NotNull Project project) throws IOException { ApplicationManager.getApplication().assertReadAccessAllowed(); - Properties res = new Properties(); - - MavenProjectsManager manager = MavenProjectsManager.getInstance(project); - - for (Module module : ModuleManager.getInstance(project).getModules()) { - if (manager.isMavenizedModule(module)) { - MavenProject mavenProject = manager.findProject(module); - if (mavenProject != null && !manager.isIgnored(mavenProject)) { - res.setProperty(mavenProject.getMavenId().getGroupId() - + ':' + mavenProject.getMavenId().getArtifactId() - + ":pom" - + ':' + mavenProject.getMavenId().getVersion(), - mavenProject.getFile().getPath()); - - res.setProperty(mavenProject.getMavenId().getGroupId() - + ':' + mavenProject.getMavenId().getArtifactId() - + ':' + mavenProject.getPackaging() - + ':' + mavenProject.getMavenId().getVersion(), - mavenProject.getOutputDirectory()); - - res.setProperty(mavenProject.getMavenId().getGroupId() - + ':' + mavenProject.getMavenId().getArtifactId() - + ":test-jar" - + ':' + mavenProject.getMavenId().getVersion(), - mavenProject.getTestOutputDirectory()); - - addArtifactFileMapping(res, mavenProject, "sources"); - addArtifactFileMapping(res, mavenProject, "test-sources"); - addArtifactFileMapping(res, mavenProject, "javadoc"); - addArtifactFileMapping(res, mavenProject, "test-javadoc"); - } - } - } + Properties res = getProjectModuleMap(project); File file = new File(PathManager.getSystemPath(), "Maven/idea-projects-state-" + project.getLocationHash() + ".properties"); FileUtil.ensureExists(file.getParentFile()); @@ -304,6 +275,43 @@ public final class MavenExternalParameters { return file; } + public static @NotNull Properties getProjectModuleMap(@NotNull Project project) { + Properties res = new Properties(); + + MavenProjectsManager manager = MavenProjectsManager.getInstance(project); + + for (MavenProject mavenProject : manager.getProjects()) { + if (!manager.isIgnored(mavenProject)) { + res.setProperty(mavenProject.getMavenId().getGroupId() + + ':' + mavenProject.getMavenId().getArtifactId() + + ":pom" + + ':' + mavenProject.getMavenId().getVersion(), + mavenProject.getFile().getPath()); + + if ("pom".equals(mavenProject.getPackaging())) { + continue; + } + res.setProperty(mavenProject.getMavenId().getGroupId() + + ':' + mavenProject.getMavenId().getArtifactId() + + ':' + mavenProject.getPackaging() + + ':' + mavenProject.getMavenId().getVersion(), + mavenProject.getOutputDirectory()); + + res.setProperty(mavenProject.getMavenId().getGroupId() + + ':' + mavenProject.getMavenId().getArtifactId() + + ":test-jar" + + ':' + mavenProject.getMavenId().getVersion(), + mavenProject.getTestOutputDirectory()); + + addArtifactFileMapping(res, mavenProject, "sources"); + addArtifactFileMapping(res, mavenProject, "test-sources"); + addArtifactFileMapping(res, mavenProject, "javadoc"); + addArtifactFileMapping(res, mavenProject, "test-javadoc"); + } + } + return res; + } + private static void addArtifactFileMapping(@NotNull Properties res, @NotNull MavenProject mavenProject, @NotNull String classifier) { File file = new File(mavenProject.getBuildDirectory(), mavenProject.getFinalName() + '-' + classifier + ".jar"); if (file.exists()) { @@ -315,7 +323,7 @@ public final class MavenExternalParameters { } } - private static @NotNull Sdk getJdk(@Nullable Project project, String jreName, boolean isGlobalRunnerSettings) + public static @NotNull Sdk getJdk(@Nullable Project project, String jreName, boolean isGlobalRunnerSettings) throws ExecutionException { if (jreName.equals(MavenRunnerSettings.USE_INTERNAL_JAVA)) { return JavaAwareProjectJdkTableImpl.getInstanceEx().getInternalJdk(); @@ -422,8 +430,8 @@ public final class MavenExternalParameters { * @param runConfiguration used to creation fix if maven home not found */ public static @NotNull @NlsSafe String resolveMavenHome(@NotNull MavenGeneralSettings coreSettings, - @NotNull Project project, - @NotNull String workingDir, @Nullable MavenRunConfiguration runConfiguration) + @NotNull Project project, + @NotNull String workingDir, @Nullable MavenRunConfiguration runConfiguration) throws ExecutionException { MavenHomeType type = coreSettings.getMavenHomeType(); Path file = null; diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/execution/run/MavenShCommandLineState.kt b/plugins/maven/src/main/java/org/jetbrains/idea/maven/execution/run/MavenShCommandLineState.kt index 378df7469542..d2d0a0685e0f 100644 --- a/plugins/maven/src/main/java/org/jetbrains/idea/maven/execution/run/MavenShCommandLineState.kt +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/execution/run/MavenShCommandLineState.kt @@ -1,4 +1,5 @@ // 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.idea.maven.execution.run import com.intellij.build.* @@ -19,8 +20,10 @@ import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.execution.runners.ProgramRunner import com.intellij.execution.testDiscovery.JvmToggleAutoTestAction import com.intellij.execution.ui.ConsoleView +import com.intellij.execution.util.ProgramParametersUtil import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.application.readAction import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskType import com.intellij.openapi.externalSystem.service.execution.ExternalSystemRunConfigurationViewManager @@ -37,8 +40,10 @@ import com.intellij.platform.eel.provider.utils.EelPathUtils.TransferTarget import com.intellij.platform.eel.provider.utils.EelPathUtils.transferLocalContentToRemote import com.intellij.platform.ide.progress.runWithModalProgressBlocking import com.intellij.psi.search.ExecutionSearchScopes -import com.intellij.util.containers.with +import com.intellij.util.io.Compressor +import com.intellij.util.io.outputStream import com.intellij.util.text.nullize +import org.jetbrains.idea.maven.artifactResolver.common.MavenModuleMap import org.jetbrains.idea.maven.buildtool.BuildToolConsoleProcessAdapter import org.jetbrains.idea.maven.buildtool.MavenBuildEventProcessor import org.jetbrains.idea.maven.execution.* @@ -48,14 +53,17 @@ import org.jetbrains.idea.maven.project.* import org.jetbrains.idea.maven.server.* import org.jetbrains.idea.maven.utils.MavenLog import org.jetbrains.idea.maven.utils.MavenUtil -import java.io.File +import org.jetbrains.idea.maven.utils.MavenUtil.isRunningFromSources +import java.io.BufferedOutputStream import java.io.IOException import java.nio.charset.Charset import java.nio.charset.UnsupportedCharsetException import java.nio.file.Path import java.util.function.Function import kotlin.io.path.Path +import kotlin.io.path.absolutePathString import kotlin.io.path.exists +import kotlin.io.path.isRegularFile class MavenShCommandLineState(val environment: ExecutionEnvironment, private val myConfiguration: MavenRunConfiguration) : RunProfileState, RemoteConnectionCreator { private var mavenConnectionWrapper: MavenRemoteConnectionWrapper? = null @@ -73,14 +81,11 @@ class MavenShCommandLineState(val environment: ExecutionEnvironment, private val val env = getEnv(eelApi.exec.fetchLoginShellEnvVariables(), debug) val charset = tryToGetCharset(env, eelApi) - val envWithCharsets = if (charset != null) { - env - } - else { - env.with("JAVA_TOOL_OPTIONS", listOfNotNull(env["JAVA_TOOL_OPTIONS"], "-Dfile.encoding=UTF-8").joinToString(" ")) + if (charset != null) { + env["JAVA_TOOL_OPTIONS"] = listOfNotNull(env["JAVA_TOOL_OPTIONS"], "-Dfile.encoding=UTF-8").joinToString(" ") } - val processHandler = runProcessInEel(eelApi, exe, envWithCharsets, charset ?: Charsets.UTF_8) + val processHandler = runProcessInEel(eelApi, exe, env, charset ?: Charsets.UTF_8) JavaRunConfigurationExtensionManager.instance .attachExtensionsToProcess(myConfiguration, processHandler, environment.runnerSettings) return@runWithModalProgressBlocking processHandler @@ -129,7 +134,7 @@ class MavenShCommandLineState(val environment: ExecutionEnvironment, private val @SuppressWarnings("IO_FILE_USAGE") // Here should be java.io.File, since we do not support remote eel run on windows Agent - private fun writeParamsToBatBecauseCmdIsReallyReallyBad(params: ParametersList, isSpyDebug: Boolean): File { + private fun writeParamsToBatBecauseCmdIsReallyReallyBad(params: ParametersList, isSpyDebug: Boolean): java.io.File { val tmpData = StringBuilder() val mapEnv = LinkedHashMap() @@ -169,7 +174,7 @@ class MavenShCommandLineState(val environment: ExecutionEnvironment, private val tmpData.append(commandPrefix).append(batParams.list.joinToString(" ") { CommandLineUtil.escapeParameterOnWindows(it, true) }) val tempDirectory = FileUtilRt.getTempDirectory() - val tmpBat = FileUtil.createTempFile(File(tempDirectory), "mvn-idea-exec", ".bat", false, true) + val tmpBat = FileUtil.createTempFile(java.io.File(tempDirectory), "mvn-idea-exec", ".bat", false, true) tmpBat.writeText(tmpData.toString()) @@ -402,27 +407,80 @@ class MavenShCommandLineState(val environment: ExecutionEnvironment, private val private fun addIdeaParameters(args: ParametersList, eel: EelApi) { args.addProperty("idea.version", MavenUtil.getIdeaVersionToPassToMavenProcess()) - args.addProperty( - MavenServerEmbedder.MAVEN_EXT_CLASS_PATH, - transferLocalContentToRemote( - source = MavenServerManager.getInstance().getMavenEventListener().toPath(), - target = TransferTarget.Temporary(eel.descriptor) - ).asEelPath().toString() - ) + addMavenEventListener(args, eel) + createDumpFileIfRequired(args, eel) args.addProperty("jansi.passthrough", "true") args.addProperty("style.color", "always") } - private fun getEnv(existingEnv: Map, debug: Boolean): Map { + private fun createDumpFileIfRequired(args: ParametersList, eel: EelApi) { + if (!myConfiguration.runnerParameters.isResolveToWorkspace) { + return + } + val map = MavenExternalParameters.getProjectModuleMap(myConfiguration.project) + val remoteMapFile = transferLocalContentToRemote( + source = FileUtil.createTempFile("idea-" + myConfiguration.project.getLocationHash(), "-mvn.properties").toPath(), + target = TransferTarget.Temporary(eel.descriptor) + ) + + remoteMapFile.outputStream().use { os -> + map.store(BufferedOutputStream(os), null) + } + + args.addProperty(MavenModuleMap.PATHS_FILE_PROPERTY, remoteMapFile.absolutePathString()) + + } + + private fun addMavenEventListener(args: ParametersList, eel: EelApi) { + val path = prepareMavenListener() + + args.addProperty( + MavenServerEmbedder.MAVEN_EXT_CLASS_PATH, + transferLocalContentToRemote( + source = path, + target = TransferTarget.Temporary(eel.descriptor) + ).asEelPath().toString() + ) + } + + private fun prepareMavenListener(): Path { + val pathToMavenListener = MavenServerManager.getInstance().getMavenEventListener().toPath() + if (!pathToMavenListener.exists()) { + throw IllegalStateException("$pathToMavenListener does not exist") + } + if (pathToMavenListener.isRegularFile()) return pathToMavenListener + + if (isRunningFromSources()) { + val tempFile = FileUtil.createTempFile("idea", "-event-listener.jar", true).toPath() + MavenLog.LOG.warn("compressing maven event listener from $pathToMavenListener") + Compressor.Zip(tempFile) + .use { zip -> + zip.addDirectory(MavenServerManager.getInstance().getMavenEventListener().toPath()) + } + return tempFile + } + throw IllegalStateException("$pathToMavenListener does not exist") + } + + private suspend fun getJavaHome(): String? { + val defaultSettings = MavenRunner.getInstance(myConfiguration.project).state + val jreName = myConfiguration.runnerSettings?.jreName ?: defaultSettings.jreName + val isGlobalRunnerSettings = defaultSettings === myConfiguration.runnerSettings + return readAction { + MavenExternalParameters.getJdk(myConfiguration.project, jreName, isGlobalRunnerSettings) + }.homePath + } + + private suspend fun getEnv(existingEnv: Map, debug: Boolean): MutableMap { val map = HashMap() map.putAll(existingEnv) myConfiguration.runnerSettings?.environmentProperties?.let { map.putAll(map) } - val javaParams = myConfiguration.createJavaParameters(myConfiguration.project) - map["JAVA_HOME"] = Path(javaParams.jdkPath).asEelPath().toString() + val javaHome = getJavaHome() ?: throw ExecutionException(MavenProjectBundle.message("exec.message.failed.to.find.maven.jdk")) + map["JAVA_HOME"] = javaHome val optsBuilder = StringBuilder(map["MAVEN_OPTS"] ?: "") myConfiguration.runnerSettings?.vmOptions?.let { if (optsBuilder.isNotEmpty()) optsBuilder.append(" ") - optsBuilder.append(it) + optsBuilder.append(ProgramParametersUtil.expandPathAndMacros(it, null, myConfiguration.project)) } val mavenOpts = if (debug && mavenConnectionWrapper != null) { mavenConnectionWrapper!!.enhanceMavenOpts(optsBuilder.toString()) @@ -432,6 +490,9 @@ class MavenShCommandLineState(val environment: ExecutionEnvironment, private val } map["MAVEN_OPTS"] = mavenOpts myConfiguration.runnerSettings?.environmentProperties?.let { map.putAll(it) } + myConfiguration.runnerParameters.multimoduleDir?.nullize()?.let { + map["MAVEN_BASEDIR"] = ProgramParametersUtil.expandPathAndMacros(it, null, myConfiguration.project) + } return map } @@ -501,13 +562,6 @@ class MavenShCommandLineState(val environment: ExecutionEnvironment, private val } } - private fun getOrCreateRemoteConnection(): MavenRemoteConnectionWrapper? { - if (mavenConnectionWrapper == null) { - mavenConnectionWrapper = createRemoteConnection() - } - return mavenConnectionWrapper - } - private fun createRemoteConnection(): MavenRemoteConnectionWrapper? { for (creator in MavenExtRemoteConnectionCreator.EP_NAME.extensionList) { val connection = creator.createRemoteConnectionForScript(myConfiguration) diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/externalSystemIntegration/output/parsers/MavenEventType.java b/plugins/maven/src/main/java/org/jetbrains/idea/maven/externalSystemIntegration/output/parsers/MavenEventType.java index 6f188c7f64c2..c0af4b486792 100644 --- a/plugins/maven/src/main/java/org/jetbrains/idea/maven/externalSystemIntegration/output/parsers/MavenEventType.java +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/externalSystemIntegration/output/parsers/MavenEventType.java @@ -21,7 +21,8 @@ public enum MavenEventType { PROJECT_SKIPPED("ProjectSkipped"), PROJECT_FAILED("ProjectFailed"), ARTIFACT_RESOLVED("ARTIFACT_RESOLVED"), - ARTIFACT_DOWNLOADING("ARTIFACT_DOWNLOADING"); + ARTIFACT_DOWNLOADING("ARTIFACT_DOWNLOADING"), + IDEA_INTERNAL_MESSAGE("IDEA_INTERNAL_MESSAGE"); public final String eventName; diff --git a/plugins/maven/src/main/resources/messages/MavenProjectBundle.properties b/plugins/maven/src/main/resources/messages/MavenProjectBundle.properties index ff4ed6961d69..516faacab351 100644 --- a/plugins/maven/src/main/resources/messages/MavenProjectBundle.properties +++ b/plugins/maven/src/main/resources/messages/MavenProjectBundle.properties @@ -210,6 +210,7 @@ label.invalid.enable.maven2plugin.with.link=Maven2 support disabled, + + 4.0.0 + + test + parent + 1 + + + m1 + + + + test + m2 + 1 + + + + \ No newline at end of file diff --git a/plugins/maven/src/test/data/projects/maven-modules/m1/src/main/java/org/example/m1/ClassM1.java b/plugins/maven/src/test/data/projects/maven-modules/m1/src/main/java/org/example/m1/ClassM1.java new file mode 100644 index 000000000000..c54c725146eb --- /dev/null +++ b/plugins/maven/src/test/data/projects/maven-modules/m1/src/main/java/org/example/m1/ClassM1.java @@ -0,0 +1,10 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.example.m1; + +import org.example.m2.ClassM2; + +public class ClassM1 { + public static void main(String[] args) { + System.out.println(ClassM2.sayHello()); + } +} \ No newline at end of file diff --git a/plugins/maven/src/test/data/projects/maven-modules/m2/pom.xml b/plugins/maven/src/test/data/projects/maven-modules/m2/pom.xml new file mode 100644 index 000000000000..beaa721f94bd --- /dev/null +++ b/plugins/maven/src/test/data/projects/maven-modules/m2/pom.xml @@ -0,0 +1,14 @@ + + + 4.0.0 + + test + parent + 1 + + + m2 + + \ No newline at end of file diff --git a/plugins/maven/src/test/data/projects/maven-modules/m2/src/main/java/org/example/m2/ClassM2.java b/plugins/maven/src/test/data/projects/maven-modules/m2/src/main/java/org/example/m2/ClassM2.java new file mode 100644 index 000000000000..b659974ddb2b --- /dev/null +++ b/plugins/maven/src/test/data/projects/maven-modules/m2/src/main/java/org/example/m2/ClassM2.java @@ -0,0 +1,11 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.example.m2; + + +public class ClassM2 { + public static final String HELLO = "Hello from Module 2"; + + public static String sayHello() { + return HELLO; + } +} \ No newline at end of file diff --git a/plugins/maven/src/test/data/projects/maven-modules/pom.xml b/plugins/maven/src/test/data/projects/maven-modules/pom.xml new file mode 100644 index 000000000000..5bc81403dd31 --- /dev/null +++ b/plugins/maven/src/test/data/projects/maven-modules/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + test + parent + 1 + pom + + + 17 + 17 + UTF-8 + + + + m1 + m2 + + diff --git a/plugins/maven/src/test/java/org/jetbrains/idea/maven/execution/MavenExecutionTest.kt b/plugins/maven/src/test/java/org/jetbrains/idea/maven/execution/MavenExecutionTest.kt index 44d3a74a6bf0..f2599ef8d9e4 100644 --- a/plugins/maven/src/test/java/org/jetbrains/idea/maven/execution/MavenExecutionTest.kt +++ b/plugins/maven/src/test/java/org/jetbrains/idea/maven/execution/MavenExecutionTest.kt @@ -29,19 +29,7 @@ abstract class MavenExecutionTest : MavenExecutionTestCase() { Registry.get("maven.use.scripts").setValue(useScripts, testRootDisposable) } - @Test - fun testExternalExecutor() = runBlocking { - createProjectSubFile("src/main/java/A.java", "public class A {}") - createProjectPom(""" - test - project - 1 - """.trimIndent()) - importProjectAsync() - assertFalse(projectPath.resolve("target").exists()) - execute(MavenRunnerParameters(true, projectPath.toCanonicalPath(), null as String?, mutableListOf("compile"), emptyList())) - assertTrue(projectPath.resolve("target").exists()) - } + @Test fun testUpdatingExcludedFoldersAfterExecution() = runBlocking { diff --git a/plugins/maven/src/test/java/org/jetbrains/idea/maven/execution/RealMavenExecutionTest.kt b/plugins/maven/src/test/java/org/jetbrains/idea/maven/execution/RealMavenExecutionTest.kt index 060ff99a0654..8bc4ddbd20b6 100644 --- a/plugins/maven/src/test/java/org/jetbrains/idea/maven/execution/RealMavenExecutionTest.kt +++ b/plugins/maven/src/test/java/org/jetbrains/idea/maven/execution/RealMavenExecutionTest.kt @@ -11,19 +11,25 @@ import com.intellij.openapi.components.service import com.intellij.openapi.externalSystem.service.execution.ExternalSystemRunConfigurationViewManager import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.io.toCanonicalPath -import com.intellij.testFramework.replaceService +import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.util.io.copyRecursively import kotlinx.coroutines.runBlocking import org.jetbrains.idea.maven.MavenCustomNioRepositoryHelper import org.jetbrains.idea.maven.project.MavenWorkspaceSettingsComponent import org.jetbrains.idea.maven.project.MavenWrapper +import org.jetbrains.idea.maven.utils.MavenLog import org.junit.Test import java.nio.file.Files import java.nio.file.Path +import java.util.* +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.io.path.absolutePathString +import kotlin.io.path.exists import kotlin.io.path.isRegularFile +import kotlin.io.path.listDirectoryEntries class RealMavenExecutionTest : MavenExecutionTest() { - private val myEvents: MutableList = ArrayList() + private val myEvents: MutableList = CopyOnWriteArrayList() private lateinit var myDisposable: Disposable override fun setUp() { @@ -50,6 +56,20 @@ class RealMavenExecutionTest : MavenExecutionTest() { } } + @Test + fun testExternalExecutor() = runBlocking { + createProjectSubFile("src/main/java/A.java", "public class A {}") + createProjectPom(""" + test + project + 1 + """.trimIndent()) + importProjectAsync() + assertFalse(projectPath.resolve("target").exists()) + execute(MavenRunnerParameters(true, projectPath.toCanonicalPath(), null as String?, mutableListOf("compile"), emptyList())) + assertTrue(projectPath.resolve("target").exists()) + } + @Test fun testProjectWithVmOptionsWithoutVMOptions() = runBlocking { @@ -87,13 +107,73 @@ class RealMavenExecutionTest : MavenExecutionTest() { assertClassCompiled("org.example.Main") } + @Test + fun testProjectWithResolveToWorkspace() = runBlocking { + useProject("maven-modules") + importProjectAsync() + val executionInfo = execute( + MavenRunnerParameters(true, + projectPath.toCanonicalPath(), null as String?, + mutableListOf("compile"), + emptyList())) + assertExecSucceed(executionInfo) + assertClassCompiled("m1", "org.example.m1.ClassM1") + assertClassCompiled("m2", "org.example.m2.ClassM2") + + val failedAppRunningInfo = execute( + MavenRunnerParameters(true, + projectPath.resolve("m1").toCanonicalPath(), null as String?, + mutableListOf("exec:java", "-Dexec.mainClass=org.example.m1.ClassM1"), + emptyList())) + assertTrue("build should fail, output was: ${failedAppRunningInfo}", + failedAppRunningInfo.stdout.contains("BUILD FAILURE", true)) + + + val succeedAppRunningInfo = execute( + MavenRunnerParameters(true, + projectPath.resolve("m1").toCanonicalPath(), null as String?, + mutableListOf("exec:java", "-Dexec.mainClass=org.example.m1.ClassM1"), + emptyList()).also { it.isResolveToWorkspace = true }) + assertTrue("build should print output from class, output was: ${succeedAppRunningInfo}", + succeedAppRunningInfo.stdout.contains("Hello from Module 2", false)) + } + + @Test + fun testProjectWorkspaceMap() = runBlocking { + useProject("maven-modules") + importProjectAsync() + assertModules("parent", "m1", "m2") + val map = MavenExternalParameters.getProjectModuleMap(project) + assertEquals("should contain 7 records", 7, map.size) + + assertArtifactMapping(map, "test:parent:pom:1", "pom.xml") + assertArtifactMapping(map, "test:m2:pom:1", "m2/pom.xml") + assertArtifactMapping(map, "test:m1:pom:1", "m1/pom.xml") + assertArtifactMapping(map, "test:m2:jar:1", "m2/target/classes") + assertArtifactMapping(map, "test:m1:jar:1", "m1/target/classes") + assertArtifactMapping(map, "test:m2:test-jar:1", "m2/target/test-classes") + assertArtifactMapping(map, "test:m1:test-jar:1", "m1/target/test-classes") + } + + fun assertArtifactMapping(map: Properties, key: String, expected: String) { + val actual = map.getProperty(key) + assertNotNull("Workspace mapping should contain $key", key) + val expectedPath = projectPath.resolve(expected).absolutePathString() + val actualPath = projectPath.resolve(actual).absolutePathString() + assertEquals("Path for $key is wrong", expectedPath, actualPath) + } + private fun assertClassCompiled(className: String) { - val target = projectPath.resolve("target") + assertClassCompiled(".", className) + } + + private fun assertClassCompiled(module: String, className: String) { + val subPath = projectPath.resolve(module) + val target = subPath.resolve("target") assertTrue("No target directory was found", Files.isDirectory(target)) val classFile = target.resolve("classes").resolve(className.replace('.', '/') + ".class") assertTrue("Compiled class $classFile was not found", classFile.isRegularFile()) - } private fun assertExecFailed() { @@ -109,15 +189,29 @@ class RealMavenExecutionTest : MavenExecutionTest() { } - private fun useProject(name: String) { + private suspend fun useProject(name: String) { val projectsDataDir: Path = MavenCustomNioRepositoryHelper.originalTestDataPath.resolve("projects") val templateProjectDir = projectsDataDir.resolve(name) + assertTrue("cannot find test project $name in $templateProjectDir", Files.isDirectory(templateProjectDir)) + MavenLog.LOG.warn("copying from $templateProjectDir to ${projectPath}") Files.newDirectoryStream(templateProjectDir).use { stream -> stream.forEach { n -> - n.copyRecursively(projectPath.resolve(templateProjectDir.relativize(n))) + MavenLog.LOG.warn("copying $n as ${templateProjectDir.relativize(n)}") + val target = projectPath.resolve(templateProjectDir.relativize(n)) + n.copyRecursively(target) + LocalFileSystem.getInstance().refreshAndFindFileByNioFile(target) } } + + LocalFileSystem.getInstance().refreshAndFindFileByNioFile(projectPath.resolve("pom.xml"))?.let { + projectPom = it + } + + MavenLog.LOG.warn(" dir path = ${projectPath}") + MavenLog.LOG.warn(" dir exists = ${projectPath.exists()}") + MavenLog.LOG.warn(" dir content = ${projectPath.listDirectoryEntries()}") + } } \ No newline at end of file diff --git a/plugins/maven/src/test/java/org/jetbrains/idea/maven/execution/ScriptMavenExecutionTest.kt b/plugins/maven/src/test/java/org/jetbrains/idea/maven/execution/ScriptMavenExecutionTest.kt index a84456973d4a..a796ce2c976b 100644 --- a/plugins/maven/src/test/java/org/jetbrains/idea/maven/execution/ScriptMavenExecutionTest.kt +++ b/plugins/maven/src/test/java/org/jetbrains/idea/maven/execution/ScriptMavenExecutionTest.kt @@ -56,11 +56,14 @@ class ScriptMavenExecutionTest : MavenExecutionTest() { )) projectsManager.addManagedFiles(listOf(anotherLinkedProject)) createFakeProjectWrapper() - mavenGeneralSettings.mavenHomeType = MavenWrapper - val path = MavenDistributionsCache.resolveEmbeddedMavenHome().mavenHome.toString() - val executionInfo = execute(MavenRunnerParameters(true, anotherLinkedProject.parent.path, null as String?, mutableListOf("verify"), emptyList())) - assertTrue("Should run bundled maven ($path) in this case, but command line was: ${executionInfo.system}", - executionInfo.system.contains(if (SystemInfo.isWindows) "\\bin\\mvn.cmd" else "/bin/mvn")) + waitForImportWithinTimeout { + mavenGeneralSettings.mavenHomeType = MavenWrapper + val path = MavenDistributionsCache.resolveEmbeddedMavenHome().mavenHome.toString() + val executionInfo = execute(MavenRunnerParameters(true, anotherLinkedProject.parent.path, null as String?, mutableListOf("verify"), emptyList())) + assertTrue("Should run bundled maven ($path) in this case, but command line was: ${executionInfo.system}", + executionInfo.system.contains(if (SystemInfo.isWindows) "\\bin\\mvn.cmd" else "/bin/mvn")) + + } } private fun createFakeProjectWrapper() {