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]. 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