Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
*
Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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.
* <p>
* 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.
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public final class ToolInstallRequest {

private boolean extraInstallation;

private boolean ignoreProject;

private Step step;

/**
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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.
*/
Expand Down
2 changes: 2 additions & 0 deletions cli/src/main/java/com/devonfw/tools/ide/tool/gui/Gui.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <code>ignoreProject</code> handling.
*/
class ToolInstallRequestTest extends AbstractIdeContextTest {

/**
* Verify that a dependency request inherits <code>ignoreProject</code> 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();
}
}
Loading