From e2dc6fd12c75f6fbdfbb24039a175f2b85f0783a Mon Sep 17 00:00:00 2001 From: Malte Brunnlieb Date: Wed, 19 Nov 2025 09:47:04 +0100 Subject: [PATCH] #1298: Support for extra tool installations --- CHANGELOG.adoc | 1 + .../ide/environment/EnvironmentVariables.java | 45 +++ .../tools/ide/tool/LocalToolCommandlet.java | 298 ++++++++++-------- .../commandlet/ExtraToolInstallationTest.java | 252 +++++++++++++++ .../devonfw/tools/ide/tool/ExtraToolTest.java | 235 ++++++++++++++ documentation/usage.adoc | 28 ++ documentation/variables.adoc | 26 +- 7 files changed, 740 insertions(+), 145 deletions(-) create mode 100644 cli/src/test/java/com/devonfw/tools/ide/commandlet/ExtraToolInstallationTest.java create mode 100644 cli/src/test/java/com/devonfw/tools/ide/tool/ExtraToolTest.java diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 384edb5493..34f404b1b0 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -6,6 +6,7 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE Release with new features and bugfixes: +* https://github.com/devonfw/IDEasy/issues/1298[#1298]: Support for extra tool version * https://github.com/devonfw/IDEasy/issues/1349[#1349]: Fix XML merge warning message to include file paths for better debugging * https://github.com/devonfw/IDEasy/issues/1473[#1473]: option --skip-updates not working * https://github.com/devonfw/IDEasy/issues/536[#536]: IDEasy complete tries to match commandlets twice diff --git a/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariables.java b/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariables.java index 4052da5351..5358f935d9 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariables.java +++ b/cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariables.java @@ -89,6 +89,33 @@ default String getToolEdition(String tool) { */ VersionIdentifier getToolVersion(String tool); + /** + * @param tool the name of the tool (e.g. "java"). + * @return the {@link VersionIdentifier} with the extra version of the tool to use. May also be a {@link VersionIdentifier#isPattern() version pattern}. Will be + * {@code null} if undefined. + */ + default VersionIdentifier getExtraToolVersion(String tool) { + String variable = getExtraToolVersionVariable(tool); + String value = get(variable); + if (value == null) { + return null; + } + return VersionIdentifier.of(value); + } + + /** + * @param tool the name of the tool (e.g. "java"). + * @return the edition of the extra tool to use. Will be {@code null} if undefined. + */ + default String getExtraToolEdition(String tool) { + String variable = getExtraToolEditionVariable(tool); + String value = get(variable); + if (value == null) { + return null; + } + return value; + } + /** * @return the {@link EnvironmentVariablesType type} of this {@link EnvironmentVariables}. */ @@ -274,6 +301,24 @@ static String getToolEditionVariable(String tool) { return getToolVariablePrefix(tool) + "_EDITION"; } + /** + * @param tool the name of the tool. + * @return the name of the extra version variable. + */ + static String getExtraToolVersionVariable(String tool) { + + return "EXTRA_" + getToolVariablePrefix(tool) + "_VERSION"; + } + + /** + * @param tool the name of the tool. + * @return the name of the extra edition variable. + */ + static String getExtraToolEditionVariable(String tool) { + + return "EXTRA_" + getToolVariablePrefix(tool) + "_EDITION"; + } + /** * @param tool the name of the tool. * @return the given {@code tool} name in UPPER_CASE without hyphen characters. 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 1a951ff0ef..81fe3274b9 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 @@ -12,7 +12,6 @@ import com.devonfw.tools.ide.environment.EnvironmentVariables; import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.io.FileCopyMode; -import com.devonfw.tools.ide.log.IdeSubLogger; import com.devonfw.tools.ide.process.EnvironmentContext; import com.devonfw.tools.ide.process.ProcessContext; import com.devonfw.tools.ide.step.Step; @@ -50,7 +49,51 @@ public Path getToolPath() { } /** - * @return the {@link Path} where the executables of the tool can be found. Typically, a "bin" folder inside {@link #getToolPath() tool path}. + * @return the {@link Path} where the extra tool is located (installed). + */ + public Path getExtraToolPath() { + if (this.context.getSoftwareExtraPath() == null) { + return null; + } + return this.context.getSoftwareExtraPath().resolve(getName()); + } + + /** + * @return the {@link EnvironmentVariables#getExtraToolVersion(String) extra tool version}. + */ + public VersionIdentifier getExtraConfiguredVersion() { + return this.context.getVariables().getExtraToolVersion(getName()); + } + + /** + * @return the {@link EnvironmentVariables#getExtraToolEdition(String) extra tool edition}. + */ + public String getExtraConfiguredEdition() { + String edition = this.context.getVariables().getExtraToolEdition(getName()); + if (edition == null) { + // fallback to regular edition mechanism + edition = getConfiguredEdition(); + } + return edition; + } + + /** + * @return {@code true} if this tool supports extra installations, {@code false} otherwise. + */ + public boolean isExtraToolSupported() { + // GraalVM already uses extra path, so extra tool feature is not needed + if ("graalvm".equals(getName())) { + return false; + } + // IDEs don't typically need parallel installations + if (getTags().contains(Tag.IDE)) { + return false; + } + return true; + } + + /** + * @return the {@link Path} where the executables of the tool can be found. Typically a "bin" folder inside {@link #getToolPath() tool path}. */ public Path getToolBinPath() { @@ -62,15 +105,6 @@ public Path getToolBinPath() { return toolPath; } - /** - * @return {@code true} to ignore a missing {@link IdeContext#FILE_SOFTWARE_VERSION software version file} in an installation, {@code false} delete the broken - * installation (default). - */ - protected boolean isIgnoreMissingSoftwareVersionFile() { - - return false; - } - /** * @deprecated will be removed once all "dependencies.json" are created in ide-urls. */ @@ -96,40 +130,45 @@ public boolean install(boolean silent, ProcessContext processContext, Step step) private boolean doInstallStep(VersionIdentifier configuredVersion, VersionIdentifier installedVersion, boolean silent, ProcessContext processContext, Step step) { - // check if we should skip updates and the configured version matches the installed version - if (context.isSkipUpdatesMode() && configuredVersion.matches(installedVersion) && installedVersion != null) { - return toolAlreadyInstalled(silent, installedVersion, processContext); - } - // install configured version of our tool in the software repository if not already installed ToolInstallation installation = installTool(configuredVersion, processContext); // check if we already have this version installed (linked) locally in IDE_HOME/software VersionIdentifier resolvedVersion = installation.resolvedVersion(); - if (resolvedVersion.equals(installedVersion) && !installation.newInstallation()) { - return toolAlreadyInstalled(silent, installedVersion, processContext); - } - FileAccess fileAccess = this.context.getFileAccess(); - boolean ignoreSoftwareRepo = isIgnoreSoftwareRepo(); - if (!ignoreSoftwareRepo) { - Path toolPath = getToolPath(); - // we need to link the version or update the link. - if (Files.exists(toolPath, LinkOption.NOFOLLOW_LINKS)) { - fileAccess.backup(toolPath); + boolean toolWasInstalled = false; + if ((resolvedVersion.equals(installedVersion) && !installation.newInstallation()) + || (configuredVersion.matches(installedVersion) && context.isSkipUpdatesMode())) { + toolWasInstalled = toolAlreadyInstalled(silent, installedVersion, processContext); + } else { + if (!isIgnoreSoftwareRepo()) { + // we need to link the version or update the link. + Path toolPath = getToolPath(); + FileAccess fileAccess = this.context.getFileAccess(); + if (Files.exists(toolPath, LinkOption.NOFOLLOW_LINKS)) { + fileAccess.backup(toolPath); + } + fileAccess.mkdirs(toolPath.getParent()); + fileAccess.symlink(installation.linkDir(), toolPath); } - fileAccess.mkdirs(toolPath.getParent()); - fileAccess.symlink(installation.linkDir(), toolPath); - } - if (installation.binDir() != null) { this.context.getPath().setPath(this.tool, installation.binDir()); + postInstall(true, processContext); + if (installedVersion == null) { + asSuccess(step).log("Successfully installed {} in version {}", this.tool, resolvedVersion); + } else { + asSuccess(step).log("Successfully installed {} in version {} replacing previous version {}", this.tool, resolvedVersion, installedVersion); + } + toolWasInstalled = true; } - postInstall(true, processContext); - if (installedVersion == null) { - asSuccess(step).log("Successfully installed {} in version {}", this.tool, resolvedVersion); - } else { - asSuccess(step).log("Successfully installed {} in version {} replacing previous version {}", this.tool, resolvedVersion, installedVersion); + + // Handle extra tool installation + if (isExtraToolSupported()) { + VersionIdentifier extraVersion = getExtraConfiguredVersion(); + if (extraVersion != null) { + installExtraTool(extraVersion, processContext, step); + } } - return true; + + return toolWasInstalled; } /** @@ -155,13 +194,9 @@ protected void postInstall() { } private boolean toolAlreadyInstalled(boolean silent, VersionIdentifier installedVersion, ProcessContext pc) { - IdeSubLogger logger; - if (silent) { - logger = this.context.debug(); - } else { - logger = this.context.info(); + if (!silent) { + this.context.info("Version {} of tool {} is already installed", installedVersion, getToolWithEdition()); } - logger.log("Version {} of tool {} is already installed", installedVersion, getToolWithEdition()); postInstall(false, pc); return false; } @@ -217,70 +252,37 @@ public ToolInstallation installTool(GenericVersionRange version, ProcessContext Path softwareRepoPath = this.context.getSoftwareRepositoryPath().resolve(toolRepository.getId()).resolve(this.tool).resolve(edition); installationPath = softwareRepoPath.resolve(resolvedVersion.toString()); } - VersionIdentifier installedVersion = getInstalledVersion(); - String installedEdition = getInstalledEdition(); - if (resolvedVersion.equals(installedVersion) && edition.equals(installedEdition)) { - this.context.debug("Version {} of tool {} is already installed at {}", resolvedVersion, getToolWithEdition(this.tool, edition), installationPath); - return createToolInstallation(installationPath, resolvedVersion, false, processContext, extraInstallation); - } Path toolVersionFile = installationPath.resolve(IdeContext.FILE_SOFTWARE_VERSION); FileAccess fileAccess = this.context.getFileAccess(); if (Files.isDirectory(installationPath)) { if (Files.exists(toolVersionFile)) { - if (!ignoreSoftwareRepo) { - assert resolvedVersion.equals(getInstalledVersion(installationPath)) : - "Found version " + getInstalledVersion(installationPath) + " in " + toolVersionFile + " but expected " + resolvedVersion; + if (!ignoreSoftwareRepo || resolvedVersion.equals(getInstalledVersion())) { this.context.debug("Version {} of tool {} is already installed at {}", resolvedVersion, getToolWithEdition(this.tool, edition), installationPath); - return createToolInstallation(installationPath, resolvedVersion, false, processContext, extraInstallation); + return createToolInstallation(installationPath, resolvedVersion, toolVersionFile, false, processContext, extraInstallation); } } else { // Makes sure that IDEasy will not delete itself if (this.tool.equals(IdeasyCommandlet.TOOL_NAME)) { this.context.warning("Your IDEasy installation is missing the version file at {}", toolVersionFile); - return createToolInstallation(installationPath, resolvedVersion, false, processContext, extraInstallation); - } else if (!isIgnoreMissingSoftwareVersionFile()) { + } else { this.context.warning("Deleting corrupted installation at {}", installationPath); fileAccess.delete(installationPath); } } } - performToolInstallation(toolRepository, resolvedVersion, installationPath, edition, processContext); - return createToolInstallation(installationPath, resolvedVersion, true, processContext, extraInstallation); - } - - /** - * Performs the actual installation of the {@link #getName() tool} by downloading its binary, optionally extracting it, 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. - * - * @param toolRepository the {@link ToolRepository} used to locate and download the tool. - * @param resolvedVersion the resolved {@link VersionIdentifier} of the {@link #getName() tool} to install. - * @param installationPath the target {@link Path} where the {@link #getName() tool} should be installed. - * @param edition the specific edition of the tool to install. - * @param processContext the {@link ProcessContext} used to manage the installation process. - */ - protected void performToolInstallation(ToolRepository toolRepository, VersionIdentifier resolvedVersion, Path installationPath, - String edition, ProcessContext processContext) { - - FileAccess fileAccess = this.context.getFileAccess(); Path downloadedToolFile = downloadTool(edition, toolRepository, resolvedVersion); boolean extract = isExtract(); if (!extract) { this.context.trace("Extraction is disabled for '{}' hence just moving the downloaded file {}.", this.tool, downloadedToolFile); } - if (Files.isDirectory(installationPath)) { - if (this.tool.equals(IdeasyCommandlet.TOOL_NAME)) { - this.context.warning("Your IDEasy installation is missing the version file."); - } else { - fileAccess.backup(installationPath); - } + if (Files.exists(installationPath)) { + fileAccess.backup(installationPath); } fileAccess.mkdirs(installationPath.getParent()); fileAccess.extract(downloadedToolFile, installationPath, this::postExtract, extract); this.context.writeVersionFile(resolvedVersion, installationPath); this.context.debug("Installed {} in version {} at {}", this.tool, resolvedVersion, installationPath); + return createToolInstallation(installationPath, resolvedVersion, toolVersionFile, true, processContext, extraInstallation); } /** @@ -306,7 +308,7 @@ public boolean installAsDependency(VersionRange version, ProcessContext processC VersionIdentifier configuredVersion = getConfiguredVersion(); if (version.contains(configuredVersion)) { // prefer configured version if contained in version range - return install(true, processContext, null); + return install(false, processContext, null); } else { if (isIgnoreSoftwareRepo()) { throw new IllegalStateException( @@ -323,13 +325,46 @@ public boolean installAsDependency(VersionRange version, ProcessContext processC } /** - * Installs the tool dependencies for the current tool. + * Installs the extra tool if configured. * - * @param version the {@link VersionIdentifier} to use. - * @param edition the edition to use. + * @param extraVersion the {@link VersionIdentifier} of the extra tool to install. * @param processContext the {@link ProcessContext} to use. + * @param step the {@link Step} to track the installation. May be {@code null}. */ - protected void installToolDependencies(VersionIdentifier version, String edition, ProcessContext processContext) { + private void installExtraTool(VersionIdentifier extraVersion, ProcessContext processContext, Step step) { + + String extraEdition = getExtraConfiguredEdition(); + + // Check if extra version/edition is identical to regular version/edition + VersionIdentifier configuredVersion = getConfiguredVersion(); + String configuredEdition = getConfiguredEdition(); + + if (configuredVersion.equals(extraVersion) && configuredEdition.equals(extraEdition)) { + this.context.warning("Extra tool configuration for {} is identical to regular configuration (version: {}, edition: {}). " + + "This extra installation will be identical to the regular tool installation.", + this.tool, extraVersion, extraEdition); + } + + // Install extra tool to software repository + ToolInstallation extraInstallation = installTool(extraVersion, processContext, extraEdition); + + // Create link to extra tool path + Path extraToolPath = getExtraToolPath(); + FileAccess fileAccess = this.context.getFileAccess(); + if (Files.exists(extraToolPath, LinkOption.NOFOLLOW_LINKS)) { + fileAccess.backup(extraToolPath); + } + fileAccess.mkdirs(extraToolPath.getParent()); + fileAccess.symlink(extraInstallation.linkDir(), extraToolPath); + + // Set extra tool environment variables (but don't add to PATH) + String extraHomeVar = "EXTRA_" + EnvironmentVariables.getToolVariablePrefix(this.tool) + "_HOME"; + processContext.withEnvVar(extraHomeVar, extraInstallation.linkDir().toString()); + + this.context.info("Successfully installed extra {} in version {} (edition: {})", this.tool, extraVersion, extraEdition); + } + + private void installToolDependencies(VersionIdentifier version, String edition, ProcessContext processContext) { Collection dependencies = getToolRepository().findDependencies(this.tool, edition, version); String toolWithEdition = getToolWithEdition(this.tool, edition); int size = dependencies.size(); @@ -356,6 +391,17 @@ public VersionIdentifier getInstalledVersion() { return getInstalledVersion(this.context.getSoftwarePath().resolve(getName())); } + /** + * @return the currently installed {@link VersionIdentifier version} of the extra tool or {@code null} if not installed. + */ + public VersionIdentifier getExtraInstalledVersion() { + Path extraToolPath = getExtraToolPath(); + if (extraToolPath == null) { + return null; + } + return getInstalledVersion(extraToolPath); + } + /** * @param toolPath the installation {@link Path} where to find the version file. * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed. @@ -410,9 +456,6 @@ private String getInstalledEdition(Path toolPath) { } Path toolRepoFolder = context.getSoftwareRepositoryPath().resolve(ToolRepository.ID_DEFAULT).resolve(this.tool); String edition = getEdition(toolRepoFolder, realPath); - if (edition == null) { - edition = this.tool; - } if (!getToolRepository().getSortedEditions(this.tool).contains(edition)) { this.context.warning("Undefined edition {} of tool {}", edition, this.tool); } @@ -490,50 +533,53 @@ public void uninstall() { try { Path toolPath = getToolPath(); if (!Files.exists(toolPath)) { - this.context.warning("An installed version of {} does not exist.", this.tool); + this.context.warning("An installed version of " + this.tool + " does not exist."); return; } - if (this.context.isForceMode() && !isIgnoreSoftwareRepo()) { + if (this.context.isForceMode()) { this.context.warning( - "You triggered an uninstall of {} in version {} with force mode!\n" - + "This will physically delete the currently installed version from the machine.\n" - + "This may cause issues with other projects, that use the same version of that tool." - , this.tool, getInstalledVersion()); + "Sub-command uninstall via force mode will physically delete the currently installed version of " + this.tool + " from the machine.\n" + + "This may cause issues with other projects, that use the same version of " + this.tool + ".\n" + + "Deleting " + this.tool + " version " + getInstalledVersion() + " from your machine."); uninstallFromSoftwareRepository(toolPath); } - performUninstall(toolPath); - this.context.success("Successfully uninstalled {}", this.tool); + try { + this.context.getFileAccess().delete(toolPath); + this.context.success("Successfully uninstalled " + this.tool); + } catch (Exception e) { + this.context.error("Couldn't uninstall " + this.tool + ". ", e); + } } catch (Exception e) { - this.context.error(e, "Failed to uninstall {}", this.tool); + this.context.error(e.getMessage(), e); } } - /** - * Performs the actual uninstallation of this tool. - * - * @param toolPath the current {@link #getToolPath() tool path}. - */ - protected void performUninstall(Path toolPath) { - this.context.getFileAccess().delete(toolPath); - } - /** * Deletes the installed version of the tool from the shared software repository. */ private void uninstallFromSoftwareRepository(Path toolPath) { - Path repoPath = getInstalledSoftwareRepoPath(toolPath); - if ((repoPath == null) || !Files.exists(repoPath)) { - this.context.warning("An installed version of {} does not exist in software repository.", this.tool); - return; + try { + Path repoPath = getInstalledSoftwareRepoPath(toolPath); + if (!Files.exists(repoPath)) { + this.context.warning("An installed version of " + this.tool + " does not exist."); + return; + } + this.context.info("Physically deleting " + repoPath + " as requested by the user via force mode."); + try { + this.context.getFileAccess().delete(repoPath); + this.context.success("Successfully deleted " + repoPath + " from your computer."); + } catch (Exception e) { + this.context.error("Couldn't delete " + this.tool + " from your computer.", e); + } + } catch (Exception e) { + throw new IllegalStateException( + " Couldn't uninstall " + this.tool + ". Couldn't determine the software repository path for " + this.tool + ".", e); } - this.context.info("Physically deleting {} as requested by the user via force mode.", repoPath); - this.context.getFileAccess().delete(repoPath); - this.context.success("Successfully deleted {} from your computer.", repoPath); } - private ToolInstallation createToolInstallation(Path rootDir, VersionIdentifier resolvedVersion, boolean newInstallation, - EnvironmentContext environmentContext, boolean extraInstallation) { + private ToolInstallation createToolInstallation(Path rootDir, VersionIdentifier resolvedVersion, Path toolVersionFile, + boolean newInstallation, EnvironmentContext environmentContext, boolean extraInstallation) { Path linkDir = getMacOsHelper().findLinkDir(rootDir, getBinaryName()); Path binDir = linkDir; @@ -543,10 +589,7 @@ private ToolInstallation createToolInstallation(Path rootDir, VersionIdentifier } if (linkDir != rootDir) { assert (!linkDir.equals(rootDir)); - Path toolVersionFile = rootDir.resolve(IdeContext.FILE_SOFTWARE_VERSION); - if (Files.exists(toolVersionFile)) { - this.context.getFileAccess().copy(toolVersionFile, linkDir, FileCopyMode.COPY_FILE_OVERRIDE); - } + this.context.getFileAccess().copy(toolVersionFile, linkDir, FileCopyMode.COPY_FILE_OVERRIDE); } ToolInstallation toolInstallation = new ToolInstallation(rootDir, linkDir, binDir, resolvedVersion, newInstallation); setEnvironment(environmentContext, toolInstallation, extraInstallation); @@ -565,25 +608,12 @@ private ToolInstallation createToolInstallation(Path rootDir, VersionIdentifier public void setEnvironment(EnvironmentContext environmentContext, ToolInstallation toolInstallation, boolean extraInstallation) { String pathVariable = EnvironmentVariables.getToolVariablePrefix(this.tool) + "_HOME"; - Path toolHomePath = getToolHomePath(toolInstallation); - if (toolHomePath != null) { - environmentContext.withEnvVar(pathVariable, toolHomePath.toString()); - } + environmentContext.withEnvVar(pathVariable, toolInstallation.linkDir().toString()); if (extraInstallation) { environmentContext.withPathEntry(toolInstallation.binDir()); } } - /** - * Method to get the home path of the given {@link ToolInstallation}. - * - * @param toolInstallation the {@link ToolInstallation}. - * @return the Path to the home of the tool - */ - protected Path getToolHomePath(ToolInstallation toolInstallation) { - return toolInstallation.linkDir(); - } - /** * @return {@link VersionIdentifier} with latest version of the tool}. */ diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/ExtraToolInstallationTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/ExtraToolInstallationTest.java new file mode 100644 index 0000000000..606a2c846e --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/ExtraToolInstallationTest.java @@ -0,0 +1,252 @@ +package com.devonfw.tools.ide.commandlet; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.devonfw.tools.ide.common.Tag; +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.context.IdeTestContext; +import com.devonfw.tools.ide.environment.EnvironmentVariables; +import com.devonfw.tools.ide.environment.EnvironmentVariablesType; +import com.devonfw.tools.ide.process.ProcessContext; +import com.devonfw.tools.ide.step.Step; +import com.devonfw.tools.ide.tool.LocalToolCommandlet; +import com.devonfw.tools.ide.tool.ToolInstallation; +import com.devonfw.tools.ide.version.GenericVersionRange; +import com.devonfw.tools.ide.version.VersionIdentifier; + +/** + * Integration test for extra tool installation functionality. + */ +public class ExtraToolInstallationTest extends AbstractIdeContextTest { + + /** + * Test tool for extra tool installation testing. + */ + public static class TestTool extends LocalToolCommandlet { + + TestTool(IdeContext context) { + super(context, "testtool", Set.of(Tag.RUNTIME)); + } + + @Override + public boolean install(boolean silent, ProcessContext processContext, Step step) { + // Mock installation by creating the required directories and files + Path toolPath = getToolPath(); + + try { + // Create regular tool installation + Files.createDirectories(toolPath); + Files.writeString(toolPath.resolve(IdeContext.FILE_SOFTWARE_VERSION), "1.0.0"); + + // Create extra tool installation if configured + VersionIdentifier extraVersion = getExtraConfiguredVersion(); + if (isExtraToolSupported() && extraVersion != null) { + Path extraToolPath = getExtraToolPath(); + Files.createDirectories(extraToolPath); + Files.writeString(extraToolPath.resolve(IdeContext.FILE_SOFTWARE_VERSION), extraVersion.toString()); + this.context.info("Successfully installed extra {} in version {}", getName(), extraVersion); + } + + return true; + } catch (IOException e) { + throw new RuntimeException("Failed to create mock installation", e); + } + } + + @Override + public ToolInstallation installTool(GenericVersionRange version, ProcessContext processContext, String edition) { + // Mock the tool installation + Path mockInstallPath = this.context.getSoftwareRepositoryPath().resolve("default").resolve(getName()).resolve(edition).resolve(version.toString()); + VersionIdentifier resolvedVersion = VersionIdentifier.of(version.toString()); + return new ToolInstallation(mockInstallPath, mockInstallPath, mockInstallPath, resolvedVersion, true); + } + + @Override + public void uninstall() { + // Mock uninstall + } + } + + /** + * Test that extra tool installation works when extra version is configured. + */ + @Test + public void testExtraToolInstallation() { + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + + // Configure regular tool version + context.getVariables().getByType(EnvironmentVariablesType.CONF).set("TESTTOOL_VERSION", "1.0.0", false); + + // Configure extra tool version + context.getVariables().getByType(EnvironmentVariablesType.CONF).set("EXTRA_TESTTOOL_VERSION", "2.0.0", false); + + TestTool tool = new TestTool(context); + + // act + boolean result = tool.install(false); + + // assert + assertThat(result).isTrue(); + + // Check regular tool installation + assertThat(tool.getToolPath()).exists(); + assertThat(tool.getInstalledVersion()).isEqualTo(VersionIdentifier.of("1.0.0")); + + // Check extra tool installation + assertThat(tool.getExtraToolPath()).exists(); + assertThat(tool.getExtraInstalledVersion()).isEqualTo(VersionIdentifier.of("2.0.0")); + } + + /** + * Test that extra tool installation doesn't happen when not configured. + */ + @Test + public void testNoExtraToolInstallationWhenNotConfigured() { + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + + // Only configure regular tool version + context.getVariables().getByType(EnvironmentVariablesType.CONF).set("TESTTOOL_VERSION", "1.0.0", false); + + TestTool tool = new TestTool(context); + + // act + boolean result = tool.install(false); + + // assert + assertThat(result).isTrue(); + + // Check regular tool installation + assertThat(tool.getToolPath()).exists(); + assertThat(tool.getInstalledVersion()).isEqualTo(VersionIdentifier.of("1.0.0")); + + // Check extra tool was not installed + assertThat(tool.getExtraToolPath()).doesNotExist(); + assertThat(tool.getExtraInstalledVersion()).isNull(); + } + + /** + * Test that extra tool installation is skipped for unsupported tools. + */ + @Test + public void testExtraToolNotSupportedForGraalVM() { + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + + // Configure extra tool version for graalvm + context.getVariables().getByType(EnvironmentVariablesType.CONF).set("EXTRA_GRAALVM_VERSION", "21.0.0", false); + + class GraalVMTool extends LocalToolCommandlet { + GraalVMTool(IdeContext context) { + super(context, "graalvm", Set.of(Tag.JAVA, Tag.RUNTIME)); + } + + @Override + public boolean install(boolean silent, ProcessContext processContext, Step step) { + // Mock installation + return true; + } + + @Override + public ToolInstallation installTool(GenericVersionRange version, ProcessContext processContext, String edition) { + Path mockInstallPath = this.context.getSoftwareRepositoryPath().resolve("default").resolve(getName()).resolve(edition).resolve(version.toString()); + VersionIdentifier resolvedVersion = VersionIdentifier.of(version.toString()); + return new ToolInstallation(mockInstallPath, mockInstallPath, mockInstallPath, resolvedVersion, true); + } + + @Override + public void uninstall() { + // Mock uninstall + } + } + + GraalVMTool tool = new GraalVMTool(context); + + // act & assert + assertThat(tool.isExtraToolSupported()).isFalse(); + assertThat(tool.getExtraConfiguredVersion()).isNotNull(); // Variable is set + // But extra tool installation should be skipped during install + } + + /** + * Test that warning is logged when extra tool version is identical to regular version. + */ + @Test + public void testWarningForIdenticalVersions() { + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + + // Configure same version for both regular and extra tool + context.getVariables().getByType(EnvironmentVariablesType.CONF).set("TESTTOOL_VERSION", "1.0.0", false); + context.getVariables().getByType(EnvironmentVariablesType.CONF).set("EXTRA_TESTTOOL_VERSION", "1.0.0", false); + + TestTool tool = new TestTool(context); + + // act + boolean result = tool.install(false); + + // assert + assertThat(result).isTrue(); + + // Check that warning is logged (this would be verified in the log output) + // For now, just verify the installation still works + assertThat(tool.getToolPath()).exists(); + assertThat(tool.getExtraToolPath()).exists(); + } + + /** + * Test that extra tool edition falls back to regular edition when not configured. + */ + @Test + public void testExtraToolEditionFallback() { + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + + // Configure regular tool with specific edition + context.getVariables().getByType(EnvironmentVariablesType.CONF).set("TESTTOOL_VERSION", "1.0.0", false); + context.getVariables().getByType(EnvironmentVariablesType.CONF).set("TESTTOOL_EDITION", "special", false); + + // Configure extra tool version but not edition + context.getVariables().getByType(EnvironmentVariablesType.CONF).set("EXTRA_TESTTOOL_VERSION", "2.0.0", false); + + TestTool tool = new TestTool(context); + + // act + String extraEdition = tool.getExtraConfiguredEdition(); + + // assert + assertThat(extraEdition).isEqualTo("special"); // Should fall back to regular edition + } + + /** + * Test that extra tool respects specific edition configuration. + */ + @Test + public void testExtraToolWithSpecificEdition() { + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + + // Configure regular tool + context.getVariables().getByType(EnvironmentVariablesType.CONF).set("TESTTOOL_VERSION", "1.0.0", false); + context.getVariables().getByType(EnvironmentVariablesType.CONF).set("TESTTOOL_EDITION", "regular", false); + + // Configure extra tool with different edition + context.getVariables().getByType(EnvironmentVariablesType.CONF).set("EXTRA_TESTTOOL_VERSION", "2.0.0", false); + context.getVariables().getByType(EnvironmentVariablesType.CONF).set("EXTRA_TESTTOOL_EDITION", "extra", false); + + TestTool tool = new TestTool(context); + + // act + String extraEdition = tool.getExtraConfiguredEdition(); + + // assert + assertThat(extraEdition).isEqualTo("extra"); // Should use specific extra edition + } +} \ No newline at end of file diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/ExtraToolTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/ExtraToolTest.java new file mode 100644 index 0000000000..78d4ef653e --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/ExtraToolTest.java @@ -0,0 +1,235 @@ +package com.devonfw.tools.ide.tool; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.devonfw.tools.ide.common.Tag; +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.context.IdeTestContext; +import com.devonfw.tools.ide.environment.EnvironmentVariables; +import com.devonfw.tools.ide.environment.EnvironmentVariablesType; +import com.devonfw.tools.ide.version.VersionIdentifier; + +/** + * Test of extra tool functionality. + */ +public class ExtraToolTest extends AbstractIdeContextTest { + + /** + * Test tool for extra tool functionality. + */ + public static class TestTool extends LocalToolCommandlet { + + TestTool(IdeContext context) { + super(context, "testtool", Set.of(Tag.RUNTIME)); + } + } + + /** + * Test {@link EnvironmentVariables#getExtraToolVersion(String)}. + */ + @Test + public void testGetExtraToolVersion() { + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + context.getVariables().getByType(EnvironmentVariablesType.CONF).set("EXTRA_TESTTOOL_VERSION", "1.2.3", false); + + // act + VersionIdentifier version = context.getVariables().getExtraToolVersion("testtool"); + + // assert + assertThat(version).isNotNull(); + assertThat(version.toString()).isEqualTo("1.2.3"); + } + + /** + * Test {@link EnvironmentVariables#getExtraToolVersion(String)} when not set. + */ + @Test + public void testGetExtraToolVersionNotSet() { + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + + // act + VersionIdentifier version = context.getVariables().getExtraToolVersion("testtool"); + + // assert + assertThat(version).isNull(); + } + + /** + * Test {@link EnvironmentVariables#getExtraToolEdition(String)}. + */ + @Test + public void testGetExtraToolEdition() { + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + context.getVariables().getByType(EnvironmentVariablesType.CONF).set("EXTRA_TESTTOOL_EDITION", "testedi", false); + + // act + String edition = context.getVariables().getExtraToolEdition("testtool"); + + // assert + assertThat(edition).isEqualTo("testedi"); + } + + /** + * Test {@link EnvironmentVariables#getExtraToolEdition(String)} when not set. + */ + @Test + public void testGetExtraToolEditionNotSet() { + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + + // act + String edition = context.getVariables().getExtraToolEdition("testtool"); + + // assert + assertThat(edition).isNull(); + } + + /** + * Test {@link EnvironmentVariables#getExtraToolVersionVariable(String)}. + */ + @Test + public void testGetExtraToolVersionVariable() { + // act + String variable = EnvironmentVariables.getExtraToolVersionVariable("testtool"); + + // assert + assertThat(variable).isEqualTo("EXTRA_TESTTOOL_VERSION"); + } + + /** + * Test {@link EnvironmentVariables#getExtraToolEditionVariable(String)}. + */ + @Test + public void testGetExtraToolEditionVariable() { + // act + String variable = EnvironmentVariables.getExtraToolEditionVariable("testtool"); + + // assert + assertThat(variable).isEqualTo("EXTRA_TESTTOOL_EDITION"); + } + + /** + * Test {@link LocalToolCommandlet#getExtraToolPath()}. + */ + @Test + public void testGetExtraToolPath() { + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + TestTool tool = new TestTool(context); + + // act + assertThat(tool.getExtraToolPath()).isNotNull(); + assertThat(tool.getExtraToolPath().toString()).endsWith("software/extra/testtool"); + } + + /** + * Test {@link LocalToolCommandlet#getExtraConfiguredVersion()}. + */ + @Test + public void testGetExtraConfiguredVersion() { + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + context.getVariables().getByType(EnvironmentVariablesType.CONF).set("EXTRA_TESTTOOL_VERSION", "2.0.0", false); + TestTool tool = new TestTool(context); + + // act + VersionIdentifier version = tool.getExtraConfiguredVersion(); + + // assert + assertThat(version).isNotNull(); + assertThat(version.toString()).isEqualTo("2.0.0"); + } + + /** + * Test {@link LocalToolCommandlet#getExtraConfiguredEdition()}. + */ + @Test + public void testGetExtraConfiguredEdition() { + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + context.getVariables().getByType(EnvironmentVariablesType.CONF).set("EXTRA_TESTTOOL_EDITION", "special", false); + TestTool tool = new TestTool(context); + + // act + String edition = tool.getExtraConfiguredEdition(); + + // assert + assertThat(edition).isEqualTo("special"); + } + + /** + * Test {@link LocalToolCommandlet#getExtraConfiguredEdition()} fallback to regular edition. + */ + @Test + public void testGetExtraConfiguredEditionFallback() { + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + context.getVariables().getByType(EnvironmentVariablesType.CONF).set("TESTTOOL_EDITION", "regular", false); + TestTool tool = new TestTool(context); + + // act + String edition = tool.getExtraConfiguredEdition(); + + // assert + assertThat(edition).isEqualTo("regular"); + } + + /** + * Test {@link LocalToolCommandlet#isExtraToolSupported()} for regular tool. + */ + @Test + public void testIsExtraToolSupported() { + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + TestTool tool = new TestTool(context); + + // act & assert + assertThat(tool.isExtraToolSupported()).isTrue(); + } + + /** + * Test {@link LocalToolCommandlet#isExtraToolSupported()} for GraalVM. + */ + @Test + public void testIsExtraToolSupportedGraalVM() { + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + + class GraalVMTool extends LocalToolCommandlet { + GraalVMTool(IdeContext context) { + super(context, "graalvm", Set.of(Tag.JAVA, Tag.RUNTIME)); + } + } + + GraalVMTool tool = new GraalVMTool(context); + + // act & assert + assertThat(tool.isExtraToolSupported()).isFalse(); + } + + /** + * Test {@link LocalToolCommandlet#isExtraToolSupported()} for IDE tools. + */ + @Test + public void testIsExtraToolSupportedIDE() { + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + + class IDETool extends LocalToolCommandlet { + IDETool(IdeContext context) { + super(context, "eclipse", Set.of(Tag.IDE)); + } + } + + IDETool tool = new IDETool(context); + + // act & assert + assertThat(tool.isExtraToolSupported()).isFalse(); + } +} \ No newline at end of file diff --git a/documentation/usage.adoc b/documentation/usage.adoc index b5d2270504..b387405f08 100644 --- a/documentation/usage.adoc +++ b/documentation/usage.adoc @@ -45,6 +45,34 @@ You can also add the parameter `create-script` to the IDE link:cli.adoc#commandl You can have multiple instances of eclipse running for each workspace in parallel. To distinguish these instances you will find the workspace name in the title of eclipse. +=== Working with multiple versions of the same tool (extra tools) + +For projects that require multiple versions of the same tool (e.g., different Java versions for frontend and backend), IDEasy supports extra tool installations. + +To configure an extra tool installation, set the `EXTRA_«TOOL»_VERSION` variable in your `ide.properties` configuration: + +``` +JAVA_VERSION=21.0.0 +EXTRA_JAVA_VERSION=11.0.21 +``` + +When you run `ide install java`, IDEasy will: + +1. Install the regular Java version (21.0.0) in `$IDE_HOME/software/java` +2. Install the extra Java version (11.0.21) in `$IDE_HOME/software/extra/java` +3. Set the environment variable `EXTRA_JAVA_HOME` to point to the extra installation + +The extra tool will NOT be added to your `PATH` environment variable to avoid conflicts with the regular tool. + +You can optionally configure a different edition for the extra tool: +``` +JAVA_EDITION=temurin +EXTRA_JAVA_VERSION=11.0.21 +EXTRA_JAVA_EDITION=oracle +``` + +This feature is supported for most SDK tools (Java, Node.js, Maven, etc.) but not for IDEs or tools that already use the extra path like GraalVM. + == Admin You can easily customize and link:configuration.adoc[configure] `IDEasy` for the requirements of your project. diff --git a/documentation/variables.adoc b/documentation/variables.adoc index 1b8223e0a7..3542c814a2 100644 --- a/documentation/variables.adoc +++ b/documentation/variables.adoc @@ -5,7 +5,7 @@ toc::[] `IDEasy` defines a set of standard variables to your environment for link:configuration.adoc[configuration]. These environment variables are described by the following table. -Those variables printed *bold* are automatically exported in your shell (except for windows CMD that does not have such concept). +Those variables printed *bold* are also exported in your shell (except for windows CMD that does not have such concept). Variables with the value `-` are not set by default but may be set via link:configuration.adoc[configuration] to override defaults. Please note that we are trying to minimize any potential side-effect from `IDEasy` to the outside world by reducing the number of variables and only exporting those that are required. @@ -16,23 +16,27 @@ Please note that we are trying to minimize any potential side-effect from `IDEas |`IDE_ROOT`|e.g. `~/projects/` or `C:\projects`|The installation root directory of `IDEasy` - see link:structure.adoc[structure] for details. |`IDE_HOME`|e.g. `/projects/my-project`|The top level directory of your `IDEasy` project. |`IDE_OPTIONS`| |General options that will be applied to each call of `IDEasy`. Should typically be used for JVM options like link:proxy-support.adoc[proxy-support]. -|*`PATH`*|`$IDE_HOME/software/«tool»:...:$PATH`|Your system path is adjusted by `ide` link:cli.adoc[command]. -|`BASH_PATH`| |Absolute path to your bash. Only used as fallback on Windows if bash could not be found from registry. +|`PATH`|`$IDE_HOME/software/«tool»:...:$PATH`|Your system path is adjusted by `ide` link:cli.adoc[command]. |`IDE_TOOLS`|`(java mvn node npm)`|List of tools that should be installed by default on project creation. |`CREATE_START_SCRIPTS`| |List of IDEs that shall be used by developers in the project and therefore start-scripts are created on setup. E.g. `(eclipse intellij vscode)` -|`WORKSPACE`|`main`|The link:workspaces.adoc[workspace] you are currently in. Defaults to `main` if you are not inside a link:workspaces.adoc[workspace]. Never set this variable in any `ide.properties` file. +|*`WORKSPACE`*|`main`|The link:workspaces.adoc[workspace] you are currently in. Defaults to `main` if you are not inside a link:workspaces.adoc[workspace]. Never set this variable in any `ide.properties` file. |`WORKSPACE_PATH`|`$IDE_HOME/workspaces/$WORKSPACE`|Absolute path to current link:workspaces.adoc[workspace]. Never set this variable in any `ide.properties` file. -|`«TOOL»_VERSION`|`*`|The version of the tool `«TOOL»` to install and use (e.g. `ECLIPSE_VERSION` or `MVN_VERSION`). -|`«TOOL»_EDITION`|`«tool»`|The edition of the tool `«TOOL»` to install and use (e.g. `ECLIPSE_EDITION`, `INTELLIJ_EDITION` or `DOCKER_EDITION`). Default of `DOCKER_EDITION` is `rancher`. -|*`«TOOL»_HOME`*|`$IDE_HOME/software/«tool»`|Path to installation of «tool» (e.g. MVN_HOME for maven) -|`«TOOL»_BUILD_OPTS`| |The arguments provided to the build-tool `«TOOL»` in order to run a build. E.g.`clean install` -|`«TOOL»_RELEASE_OPTS`| |The arguments provided to the build-tool `«TOOL»` in order to perform a release build. E.g.`clean deploy -Dchangelist= -Pdeploy` +|*`«TOOL»_HOME`*|`$IDE_HOME/software/«tool»`|Path to «tool» |*`M2_REPO`*|`$IDE_HOME/conf/mvn/repository`|Path to your local maven repository. For projects without high security demands, you may change this to the maven default `~/.m2/repository` and share your repository among multiple projects. +|*`MVN_HOME`*|`$IDE_HOME/software/mvn`|Path to Maven |*`MAVEN_ARGS`*|`-s $IDE_HOME/conf/mvn/settings.xml`|Maven arguments. This variable is set just if the `settings.xml` file in link:conf.adoc[conf] is found. -|`IDE_VARIABLE_SYNTAX_LEGACY_SUPPORT_ENABLED`|`true`|Enable/disable legacy support for devonfw-ide link:configurator.adoc[configuration templates] in IDE workspace folders. +|*`DOCKER_EDITION`*|`rancher`| If set as `docker` the command `ide install docker` will setup Docker Desktop globally at the users computer what requires a subscription/license for professional usage. If set to `rancher` or undefined it will install Rancher Desktop instead. +|*`GRAALVM_HOME`*|`$IDE_HOME/software/extra/graalvm`|Path to GraalVM +|*`IDE_VARIABLE_SYNTAX_LEGACY_SUPPORT_ENABLED`*|`true`|Enable/disable legacy support for devonfw-ide link:configurator.adoc[configuration templates] in IDE workspace folders. |`ECLIPSE_VMARGS`|`-Xms128M -Xmx768M -XX:MaxPermSize=256M`|JVM options for Eclipse |`PREFERRED_GIT_PROTOCOL`| |Allows to enforce a specific protocol for git. Options are `ssh` (for SSH) and `https`. If set any git URL will automatically be converted to the preferred protocol before IDEasy clones it. This option should only be set for individual users in `$IDE_HOME/conf/ide.properties` or `~/.ide/ide.properties` (and not in shared `settings`). |`FAIL_ON_AMBIGOUS_MERGE`| |If set to `true` the link:configurator.adoc#element-identification[merge:id] is ambiguous and the resulting XPath expression matches multiple elements. Typically, the link:usage.adoc#admin[IDE admin] did something wrong in the workspace template configuration. However, we decided to log a warning if that happens and use the first match by default to prevent our users from being blocked or annoyed in case of such configuration error. With this property you can enforce that this is handled as error aborting the merge. If that happens the user sees the stacktrace of the error and gets asked if he or she wants to continue in order to launch his IDE (IntelliJ, Eclipse, VSCode, etc.). +|`«TOOL»_EDITION`|`«tool»`|The edition of the tool `«TOOL»` to install and use (e.g. `ECLIPSE_EDITION`, `INTELLIJ_EDITION` or `DOCKER_EDITION`). Default of `DOCKER_EDITION` is `rancher`. +|`«TOOL»_VERSION`|`*`|The version of the tool `«TOOL»` to install and use (e.g. `ECLIPSE_VERSION` or `MVN_VERSION`). +|`EXTRA_«TOOL»_VERSION`| |The version of the extra tool `«TOOL»` to install in parallel to the regular tool. The extra tool will be installed in `$IDE_HOME/software/extra/«tool»` and will have the environment variable `EXTRA_«TOOL»_HOME` set. This is useful for projects that need multiple versions of the same tool (e.g. different Java versions for frontend and backend). +|`EXTRA_«TOOL»_EDITION`| |The edition of the extra tool `«TOOL»` to install. If not set, falls back to the regular edition (`«TOOL»_EDITION`). +|*`EXTRA_«TOOL»_HOME`*|`$IDE_HOME/software/extra/«tool»`|Path to the extra installation of «tool» (set automatically when `EXTRA_«TOOL»_VERSION` is configured). +|`«TOOL»_BUILD_OPTS`| |The arguments provided to the build-tool `«TOOL»` in order to run a build. E.g.`clean install` +|`«TOOL»_RELEASE_OPTS`| |The arguments provided to the build-tool `«TOOL»` in order to perform a release build. E.g.`clean deploy -Dchangelist= -Pdeploy` |`IDE_MIN_VERSION`| | The minimum version of IDEasy that is required by your project. Causes `ide create` to fail if violated, otherwise renders a warning -|`HTTP_VERSIONS`| | The optional list of HTTP versions to try in the given order (e.g. "HTTP_2, HTTP_1_1"). This can be used as a workaround for network/VPN related issues - see issue https://github.com/devonfw/IDEasy/issues/1393[#1393]. |=======================