From becda8ba6e845ac07e44eb5d9834c98a2c941a0e Mon Sep 17 00:00:00 2001 From: caylipp Date: Thu, 9 Jul 2026 16:48:35 +0200 Subject: [PATCH 1/2] 1976: extend tool commandlet installation logic -add ToolInstallRequest ignoreProject support -skip project symlinks when ignoring project -update dependency installation to ignore project version -add CLI --ignore-project flag - update GUI launcher dependencies -add tests for ignore project flag --- .../ide/commandlet/InstallCommandlet.java | 6 ++ .../tools/ide/tool/LocalToolCommandlet.java | 29 ++++++--- .../tools/ide/tool/ToolInstallRequest.java | 19 ++++++ .../com/devonfw/tools/ide/tool/gui/Gui.java | 2 + .../ide/commandlet/InstallCommandletTest.java | 50 ++++++++++++++ .../ide/tool/LocalToolCommandletTest.java | 27 ++++++++ .../ide/tool/ToolInstallRequestTest.java | 65 +++++++++++++++++++ 7 files changed, 188 insertions(+), 10 deletions(-) create mode 100644 cli/src/test/java/com/devonfw/tools/ide/tool/ToolInstallRequestTest.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/InstallCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/InstallCommandlet.java index 57ba14ba02..52d0adc8f3 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/InstallCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/InstallCommandlet.java @@ -7,6 +7,7 @@ import com.devonfw.tools.ide.cli.GraalVmHelper; import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.property.BooleanProperty; import com.devonfw.tools.ide.property.ToolProperty; import com.devonfw.tools.ide.property.VersionProperty; import com.devonfw.tools.ide.tool.IdeasyCommandlet; @@ -31,6 +32,9 @@ public class InstallCommandlet extends Commandlet { /** The optional version to set and install. */ public final VersionProperty version; + /** Ignore the current project during installation. */ + public final BooleanProperty ignoreProject; + /** * The constructor. * @@ -42,6 +46,7 @@ public InstallCommandlet(IdeContext context) { addKeyword(getName()); this.tool = add(new ToolProperty("", false, "tool")); this.version = add(new VersionProperty("", false, "version")); + this.ignoreProject = add(new BooleanProperty("", false, "ignore-project")); } @Override @@ -86,6 +91,7 @@ protected void doRun() { } ToolInstallRequest request = ToolInstallRequest.ofDirect(); request.setRequested(new ToolEditionAndVersion(version)); + request.setIgnoreProject(this.ignoreProject.getValue()); ToolInstallation installation = commandlet.install(request); if (versionIdentifier != null) { VersionIdentifier installedVersion = installation.resolvedVersion(); diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/LocalToolCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/tool/LocalToolCommandlet.java index def9af76db..d9535650b0 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/LocalToolCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/LocalToolCommandlet.java @@ -105,13 +105,16 @@ private ToolInstallation doInstallStep(ToolInstallRequest request) { FileAccess fileAccess = this.context.getFileAccess(); boolean ignoreSoftwareRepo = isIgnoreSoftwareRepo(); Path toolPath = request.getToolPath(); - if (!ignoreSoftwareRepo) { + if (!ignoreSoftwareRepo && !request.isIgnoreProject() && toolPath != null) { // we need to link the version or update the link. if (Files.exists(toolPath, LinkOption.NOFOLLOW_LINKS)) { fileAccess.backup(toolPath); } fileAccess.mkdirs(toolPath.getParent()); fileAccess.symlink(installation.linkDir(), toolPath); + } else { + LOG.debug("Skipping symlink creation for tool {} as it is installed in standalone/global mode (ignoreSoftwareRepo={}, ignoreProject={}, toolPath={}).", + this.tool, ignoreSoftwareRepo, request.isIgnoreProject(), toolPath); } if (!request.isExtraInstallation() && (installation.binDir() != null)) { this.context.getPath().setPath(this.tool, installation.binDir()); @@ -223,8 +226,8 @@ public ToolInstallation installTool(ToolInstallRequest request) { } /** - * Performs the installation of the {@link #getName() tool} by using {@link #installDownloadedToolPayload(ToolInstallRequest, Path, Path)} - * for tool-specific logic, backing up any existing installation, and writing the version file. + * Performs the installation of the {@link #getName() tool} by using {@link #installDownloadedToolPayload(ToolInstallRequest, Path, Path)} for tool-specific + * logic, backing up any existing installation, and writing the version file. *

* This method assumes that the version has already been resolved and dependencies installed. It handles the final steps of placing the tool into the * appropriate installation directory. @@ -292,24 +295,30 @@ public ToolInstallation installAsDependency(VersionRange versionRange, ToolInsta ToolInstallRequest request = new ToolInstallRequest(parentRequest); ToolEditionAndVersion requested = new ToolEditionAndVersion(getToolWithConfiguredEdition()); request.setRequested(requested); + + // If we are not inside a project or if we should ignore the project, do not try to use the configured version but instead just use the version range from the dependency. + boolean ignoreProject = request.isIgnoreProject() || this.context.getIdeHome() == null; VersionIdentifier configuredVersion = getConfiguredVersion(); - if (versionRange.contains(configuredVersion)) { + if (!ignoreProject && (configuredVersion != null) && versionRange.contains(configuredVersion)) { // prefer configured version if contained in version range requested.setVersion(configuredVersion); // return install(true, configuredVersion, processContext, null); return install(request); } else { - if (isIgnoreSoftwareRepo()) { + if (ignoreProject) { + LOG.debug("Ignoring project for dependency {} of tool {}, using version range {}", this.tool, parentRequest.getRequested(), versionRange); + } else if (isIgnoreSoftwareRepo()) { throw new IllegalStateException( "Cannot satisfy dependency to " + this.tool + " in version " + versionRange + " for " + parentRequest.getRequested() + " since it is conflicting with configured version " + configuredVersion + " and this tool does not support the software repository."); + } else { + LOG.info( + "The tool {} requires {} in the version range {}, but your project uses version {}, which does not match." + + " Therefore, we install a compatible version in that range.", + parentRequest.getRequested().getEdition(), this.tool, versionRange, configuredVersion); } - LOG.info( - "The tool {} requires {} in the version range {}, but your project uses version {}, which does not match." - + " Therefore, we install a compatible version in that range.", - parentRequest.getRequested().getEdition(), this.tool, versionRange, configuredVersion); requested.setVersion(versionRange); return installTool(request); } @@ -604,7 +613,7 @@ protected Path findWrapper(String wrapperFileName) { public Path findBuildDescriptor(Path directory) { return null; } - + /** * @return Bash completion command for this tool or {@code null} if this tool does not provide Bash completion. */ diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/ToolInstallRequest.java b/cli/src/main/java/com/devonfw/tools/ide/tool/ToolInstallRequest.java index 083d6b47f7..73362496b9 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/ToolInstallRequest.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/ToolInstallRequest.java @@ -36,6 +36,8 @@ public final class ToolInstallRequest { private boolean extraInstallation; + private boolean ignoreProject; + private Step step; /** @@ -69,6 +71,7 @@ private ToolInstallRequest(ToolInstallRequest parent, boolean silent, boolean di this.direct = direct; if (parent != null) { this.processContext = parent.processContext; + this.ignoreProject = parent.ignoreProject; } } @@ -239,6 +242,22 @@ public boolean isExtraInstallation() { return this.extraInstallation; } + /** + * @return {@code true} if the current project should be ignored during tool installation, {@code false} otherwise. + */ + public boolean isIgnoreProject() { + + return this.ignoreProject; + } + + /** + * @param ignoreProject new value of {@link #isIgnoreProject() ignoreProject}. + */ + public void setIgnoreProject(boolean ignoreProject) { + + this.ignoreProject = ignoreProject; + } + /** * @return the {@link Step} for the installation. */ diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/gui/Gui.java b/cli/src/main/java/com/devonfw/tools/ide/tool/gui/Gui.java index 01ab0ab3c1..68c3afacdd 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/gui/Gui.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/gui/Gui.java @@ -52,11 +52,13 @@ protected void doRun() { ToolInstallRequest mavenToolInstallRequest = new ToolInstallRequest(false); mavenToolInstallRequest.setProcessContext(processContext); + mavenToolInstallRequest.setIgnoreProject(true); ToolInstallRequest javaToolInstallRequest = new ToolInstallRequest(mavenToolInstallRequest); javaToolInstallRequest.setRequested( new ToolEditionAndVersion(VersionIdentifier.of("25.*")) ); + javaToolInstallRequest.setIgnoreProject(true); mvn.installTool(mavenToolInstallRequest); ToolInstallation javaInstallation = java.installTool(javaToolInstallRequest); diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/InstallCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/InstallCommandletTest.java index a453275307..f2a4322f3a 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/InstallCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/InstallCommandletTest.java @@ -302,5 +302,55 @@ public void testInstallCommandletOfflineUpdateWithoutCachedDownload(WireMockRunt assertThat(context).logAtWarning().hasMessage("Cannot download java in version " + targetVersion + " because we are offline. Continuing with already installed version " + installedVersion + "."); } + + /** + * Test of {@link InstallCommandlet} with --ignore-project flag. Verifies that the tool is installed in the software repository without creating a symlink + * inside the project, even though we are currently inside a project context. + * + * @param wmRuntimeInfo wireMock server on a random port + */ + @Test + void testInstallCommandletWithIgnoreProjectDoesNotCreateSymlink(WireMockRuntimeInfo wmRuntimeInfo) { + + // arrange + IdeTestContext context = newContext(PROJECT_INSTALL, wmRuntimeInfo); + InstallCommandlet install = context.getCommandletManager().getCommandlet(InstallCommandlet.class); + install.tool.setValueAsString("java", context); + install.ignoreProject.setValue(true); + + // act + install.run(); + + // assert - java should be installed in the software repository + assertThat(context.getSoftwareRepositoryPath().resolve(DefaultToolRepository.ID_DEFAULT).resolve("java").resolve("java") + .resolve("17.0.6")).exists(); + // assert - no project symlink should have been created + assertThat(context.getSoftwarePath().resolve("java")).doesNotExist(); + assertThat(context).logAtDebug().hasMessageContaining("Skipping symlink creation"); + } + + /** + * Test of {@link InstallCommandlet} with --ignore-project flag outside a project. Verifies that dependencies are still installed into the software repository. + * + * @param wmRuntimeInfo wireMock server on a random port + */ + @Test + void testInstallCommandletWithIgnoreProjectOutsideProject(WireMockRuntimeInfo wmRuntimeInfo) { + + // arrange - context without IDEasy project root + IdeTestContext context = newContext(PROJECT_INSTALL, wmRuntimeInfo); + context.setIdeHome(null); + InstallCommandlet install = context.getCommandletManager().getCommandlet(InstallCommandlet.class); + install.tool.setValueAsString("java", context); + install.ignoreProject.setValue(true); + + // act + install.run(); + + // assert - tool should be installed in the software repository without requiring a project symlink + assertThat(context.getSoftwareRepositoryPath().resolve(DefaultToolRepository.ID_DEFAULT).resolve("java").resolve("java") + .resolve("17.0.6")).exists(); + assertThat(context.getSoftwarePath()).isNull(); + } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/LocalToolCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/LocalToolCommandletTest.java index c5f3f34df5..697f848e6e 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/tool/LocalToolCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/LocalToolCommandletTest.java @@ -13,6 +13,9 @@ import com.devonfw.tools.ide.os.SystemInfoMock; import com.devonfw.tools.ide.tool.intellij.Intellij; import com.devonfw.tools.ide.tool.repository.ToolRepository; +import com.devonfw.tools.ide.version.BoundaryType; +import com.devonfw.tools.ide.version.VersionIdentifier; +import com.devonfw.tools.ide.version.VersionRange; /** * Test of {@link LocalToolCommandlet}. @@ -129,6 +132,30 @@ void testRunToolWithDependencies() { runIntellijAndCheckInstallationWithJavaDependency(context); } + /** + * Test dependency installation ignoring the project configuration. + */ + @Test + void testInstallAsDependencyIgnoresProject() { + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + LocalToolDummyCommandlet dependencyTool = new LocalToolDummyCommandlet(context); + ToolInstallRequest parentRequest = new ToolInstallRequest(false); + parentRequest.setIgnoreProject(true); + parentRequest.setRequested(new ToolEditionAndVersion(VersionIdentifier.of("25.*"))); + VersionRange versionRange = VersionRange.of(VersionIdentifier.of("17.0.6"), VersionIdentifier.of("25.0.9"), BoundaryType.CLOSED); + + // act + ToolInstallation installation = dependencyTool.installAsDependency(versionRange, parentRequest); + + // assert + assertThat(installation.newInstallation()).isTrue(); + assertThat(installation.rootDir()).isEqualTo( + context.getSoftwareRepositoryPath().resolve(ToolRepository.ID_DEFAULT).resolve("dummy").resolve("dummy").resolve("25.0.9")); + assertThat(context.getSoftwarePath().resolve("dummy")).doesNotExist(); + assertThat(context).logAtDebug().hasMessageContaining("Ignoring project for dependency"); + } + private static void runIntellijAndCheckInstallationWithJavaDependency(IdeTestContext context) { // arrange context.getTestStartContext().getEntries().clear(); // clear logs from previous run(s) diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/ToolInstallRequestTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/ToolInstallRequestTest.java new file mode 100644 index 0000000000..f107bc8242 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/ToolInstallRequestTest.java @@ -0,0 +1,65 @@ +package com.devonfw.tools.ide.tool; + +import org.junit.jupiter.api.Test; + +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeTestContext; +import com.devonfw.tools.ide.process.ProcessContext; + +/** + * Tests for {@link ToolInstallRequest} parent state inheritance and ignoreProject handling. + */ +class ToolInstallRequestTest extends AbstractIdeContextTest { + + /** + * Verify that a dependency request inherits ignoreProject and process context from its parent request. + */ + @Test + void testParentIgnoreProjectIsInherited() { + // arrange + ToolInstallRequest parent = new ToolInstallRequest(false); + parent.setIgnoreProject(true); + IdeTestContext context = newContext(PROJECT_BASIC); + ProcessContext processContext = context.newProcess(); + parent.setProcessContext(processContext); + + // act + ToolInstallRequest dependencyRequest = new ToolInstallRequest(parent); + + // assert + assertThat(dependencyRequest.isIgnoreProject()).isTrue(); + assertThat(dependencyRequest.getProcessContext()).isEqualTo(processContext); + } + + /** + * Verify that creating a dependency request with a null parent does not crash and defaults ignoreProject to false. + */ + @Test + void testDependencyRequestWithNullParent() { + // arrange & act + ToolInstallRequest dependencyRequest = new ToolInstallRequest(null); + + // assert + assertThat(dependencyRequest.isIgnoreProject()).isFalse(); + assertThat(dependencyRequest.getProcessContext()).isNull(); + } + + /** + * Verify that {@link ToolInstallRequest#isIgnoreProject() ignoreProject} can be changed after creating a request. + */ + @Test + @SuppressWarnings("ConstantConditions") + void testIgnoreProjectCanBeChanged() { + // arrange + ToolInstallRequest request = new ToolInstallRequest(false); + assertThat(request.isIgnoreProject()).isFalse(); + + // act & assert - toggle to true + request.setIgnoreProject(true); + assertThat(request.isIgnoreProject()).isTrue(); + + // act & assert - toggle back to false + request.setIgnoreProject(false); + assertThat(request.isIgnoreProject()).isFalse(); + } +} \ No newline at end of file From 8326ee8514526a64927b2611aa10d7aff859d83f Mon Sep 17 00:00:00 2001 From: caylipp Date: Thu, 9 Jul 2026 17:03:09 +0200 Subject: [PATCH 2/2] 1976: add CHANGELOG entry --- CHANGELOG.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 918ac0f87e..c5911183c8 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -5,6 +5,7 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE == 2026.07.002 Release with new features and bugfixes: +* https://github.com/devonfw/IDEasy/issues/1976[#1976]: Extend Tool Commandlet installation logic The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/47?closed=1[milestone 2026.07.002].