diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java index d4d9b20fb..704c1815c 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java @@ -1,5 +1,19 @@ package com.devonfw.tools.ide.context; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.InetAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + import com.devonfw.tools.ide.cli.CliAbortException; import com.devonfw.tools.ide.cli.CliArgument; import com.devonfw.tools.ide.cli.CliArguments; @@ -36,18 +50,6 @@ import com.devonfw.tools.ide.step.StepImpl; import com.devonfw.tools.ide.url.model.UrlMetadata; -import java.io.IOException; -import java.net.InetAddress; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.function.Function; - /** * Abstract base implementation of {@link IdeContext}. */ @@ -929,4 +931,65 @@ public boolean apply(CliArguments arguments, Commandlet cmd, CompletionCandidate return true; } + public String findBash() { + + String bash = "bash"; + if (SystemInfoImpl.INSTANCE.isWindows()) { + bash = findBashOnWindows(); + } + + return bash; + } + + private String findBashOnWindows() { + + // Check if Git Bash exists in the default location + Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe"); + if (Files.exists(defaultPath)) { + return defaultPath.toString(); + } + + // If not found in the default location, try the registry query + String[] bashVariants = { "GitForWindows", "Cygwin\\setup" }; + String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" }; + String regQueryResult; + for (String bashVariant : bashVariants) { + for (String registryKey : registryKeys) { + String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir"; + String command = "reg query " + registryKey + "\\Software\\" + bashVariant + " /v " + toolValueName + " 2>nul"; + + try { + Process process = new ProcessBuilder("cmd.exe", "/c", command).start(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + StringBuilder output = new StringBuilder(); + String line; + + while ((line = reader.readLine()) != null) { + output.append(line); + } + + int exitCode = process.waitFor(); + if (exitCode != 0) { + return null; + } + + regQueryResult = output.toString(); + if (regQueryResult != null) { + int index = regQueryResult.indexOf("REG_SZ"); + if (index != -1) { + String path = regQueryResult.substring(index + "REG_SZ".length()).trim(); + return path + "\\bin\\bash.exe"; + } + } + + } + } catch (Exception e) { + return null; + } + } + } + // no bash found + throw new IllegalStateException("Could not find Bash. Please install Git for Windows and rerun."); + } + } diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java index ff62beaff..e8905f261 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java @@ -471,4 +471,12 @@ default void setIdeHome(Path ideHome) { * @param ideHome The path to the IDE home directory. */ void setCwd(Path userDir, String workspace, Path ideHome); + + /** + * Finds the path to the Bash executable. + * + * @return the {@link String} to the Bash executable, or {@code null} if Bash is not found + */ + String findBash(); + } diff --git a/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java b/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java index 72b0a3d4b..26240e5dd 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java @@ -245,57 +245,6 @@ private String getSheBang(Path file) { return null; } - private String findBashOnWindows() { - - // Check if Git Bash exists in the default location - Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe"); - if (Files.exists(defaultPath)) { - return defaultPath.toString(); - } - - // If not found in the default location, try the registry query - String[] bashVariants = { "GitForWindows", "Cygwin\\setup" }; - String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" }; - String regQueryResult; - for (String bashVariant : bashVariants) { - for (String registryKey : registryKeys) { - String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir"; - String command = "reg query " + registryKey + "\\Software\\" + bashVariant + " /v " + toolValueName + " 2>nul"; - - try { - Process process = new ProcessBuilder("cmd.exe", "/c", command).start(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { - StringBuilder output = new StringBuilder(); - String line; - - while ((line = reader.readLine()) != null) { - output.append(line); - } - - int exitCode = process.waitFor(); - if (exitCode != 0) { - return null; - } - - regQueryResult = output.toString(); - if (regQueryResult != null) { - int index = regQueryResult.indexOf("REG_SZ"); - if (index != -1) { - String path = regQueryResult.substring(index + "REG_SZ".length()).trim(); - return path + "\\bin\\bash.exe"; - } - } - - } - } catch (Exception e) { - return null; - } - } - } - // no bash found - throw new IllegalStateException("Could not find Bash. Please install Git for Windows and rerun."); - } - private String addExecutable(String exec, List args) { String interpreter = null; @@ -317,19 +266,12 @@ private String addExecutable(String exec, List args) { } } if (isBashScript) { - String bash = "bash"; - interpreter = bash; - // here we want to have native OS behavior even if OS is mocked during tests... - if (SystemInfoImpl.INSTANCE.isWindows()) { - String findBashOnWindowsResult = findBashOnWindows(); - if (findBashOnWindowsResult != null) { - bash = findBashOnWindowsResult; - } - } - args.add(bash); - } else if (SystemInfoImpl.INSTANCE.isWindows() && "msi".equalsIgnoreCase(fileExtension)) { - args.add("msiexec"); - args.add("/i"); + interpreter = "bash"; + args.add(this.context.findBash()); + } + if ("msi".equalsIgnoreCase(fileExtension)) { + args.add(0, "/i"); + args.add(0, "msiexec"); } args.add(exec); return interpreter; @@ -365,22 +307,12 @@ private void modifyArgumentsOnBackgroundProcess(ProcessMode processMode) { throw new IllegalStateException("Cannot handle non background process mode!"); } - String bash = "bash"; - - // try to use bash in windows to start the process - if (this.context.getSystemInfo().isWindows()) { - - String findBashOnWindowsResult = findBashOnWindows(); - if (findBashOnWindowsResult != null) { - - bash = findBashOnWindowsResult; - - } else { - this.context.warning( - "Cannot start background process in windows! No bash installation found, output will be discarded."); - this.processBuilder.redirectOutput(Redirect.DISCARD).redirectError(Redirect.DISCARD); - return; - } + String bash = this.context.findBash(); + if (bash == null) { + context.warning( + "Cannot start background process via bash because no bash installation was found. Hence, output will be discarded."); + this.processBuilder.redirectOutput(Redirect.DISCARD).redirectError(Redirect.DISCARD); + return; } String commandToRunInBackground = buildCommandToRunInBackground(); diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/GlobalToolCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/tool/GlobalToolCommandlet.java index 62b5afd7d..f433093df 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/GlobalToolCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/GlobalToolCommandlet.java @@ -2,8 +2,8 @@ import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.Set; import com.devonfw.tools.ide.common.Tag; @@ -26,7 +26,7 @@ public abstract class GlobalToolCommandlet extends ToolCommandlet { * @param context the {@link IdeContext}. * @param tool the {@link #getName() tool name}. * @param tags the {@link #getTags() tags} classifying the tool. Should be created via {@link Set#of(Object) Set.of} - * method. + * method. */ public GlobalToolCommandlet(IdeContext context, String tool, Set tags) { @@ -36,57 +36,63 @@ public GlobalToolCommandlet(IdeContext context, String tool, Set tags) { /** * Performs the installation of the {@link #getName() tool} via a package manager. * - * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise. - * @param commands - A {@link Map} containing the commands used to perform the installation for each package manager. - * @return {@code true} if the tool was newly installed, {@code false} if the tool was already installed before and - * nothing has changed. + * @param silent {@code true} if called recursively to suppress verbose logging, {@code false} otherwise. + * @param commandStrings commandStrings The package manager command strings to execute. + * @return {@code true} if installation succeeds with any of the package manager commands, {@code false} otherwise. */ - protected boolean installWithPackageManger(Map> commands, boolean silent) { + protected boolean installWithPackageManager(boolean silent, String... commandStrings) { - Path binaryPath = this.context.getPath().findBinary(Path.of(getBinaryName())); - - if (binaryPath != null && Files.exists(binaryPath) && !this.context.isForceMode()) { - IdeLogLevel level = silent ? IdeLogLevel.DEBUG : IdeLogLevel.INFO; - this.context.level(level).log("{} is already installed at {}", this.tool, binaryPath); - return false; - } + List pmCommands = Arrays.stream(commandStrings).map(PackageManagerCommand::of).toList(); + return installWithPackageManager(silent, pmCommands); + } - Path bashPath = this.context.getPath().findBinary(Path.of("bash")); - if (bashPath == null || !Files.exists(bashPath)) { - context.warning("Bash was not found on this machine. Not Proceeding with installation of tool " + this.tool); - return false; - } + /** + * Performs the installation of the {@link #getName() tool} via a package manager. + * + * @param silent {@code true} if called recursively to suppress verbose logging, {@code false} otherwise. + * @param pmCommands A list of {@link PackageManagerCommand} to be used for installation. + * @return {@code true} if installation succeeds with any of the package manager commands, {@code false} otherwise. + */ + protected boolean installWithPackageManager(boolean silent, List pmCommands) { + + for (PackageManagerCommand pmCommand : pmCommands) { + PackageManager packageManager = pmCommand.packageManager(); + Path packageManagerPath = this.context.getPath().findBinary(Path.of(packageManager.getBinaryName())); + if (packageManagerPath == null || !Files.exists(packageManagerPath)) { + this.context.debug("{} is not installed", packageManager.toString()); + continue; // Skip to the next package manager command + } - PackageManager foundPackageManager = null; - for (PackageManager pm : commands.keySet()) { - if (Files.exists(this.context.getPath().findBinary(Path.of(pm.toString().toLowerCase())))) { - foundPackageManager = pm; - break; + if (executePackageManagerCommand(pmCommand, silent)) { + return true; // Successfully installed } } + return false; // None of the package manager commands were successful + } - int finalExitCode = 0; - if (foundPackageManager == null) { - context.warning("No supported Package Manager found for installation"); - return false; - } else { - List commandList = commands.get(foundPackageManager); - if (commandList != null) { - for (String command : commandList) { - ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.WARNING).executable(bashPath) - .addArgs("-c", command); - finalExitCode = pc.run(); - } + /** + * Executes the provided package manager command. + * + * @param pmCommand The {@link PackageManagerCommand} containing the commands to execute. + * @param silent {@code true} if called recursively to suppress verbose logging, {@code false} otherwise. + * @return {@code true} if the package manager commands execute successfully, {@code false} otherwise. + */ + private boolean executePackageManagerCommand(PackageManagerCommand pmCommand, boolean silent) { + + String bashPath = this.context.findBash(); + for (String command : pmCommand.commands()) { + ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.WARNING).executable(bashPath) + .addArgs("-c", command); + int exitCode = pc.run(); + if (exitCode != 0) { + this.context.warning("{} command did not execute successfully", command); + return false; } } - if (finalExitCode == 0) { + if (!silent) { this.context.success("Successfully installed {}", this.tool); - } else { - this.context.warning("{} was not successfully installed", this.tool); - return false; } - postInstall(); return true; } @@ -122,7 +128,7 @@ protected boolean doInstall(boolean silent) { tmpDir = fileAccess.createTempDir(getName()); Path downloadBinaryPath = tmpDir.resolve(target.getFileName()); fileAccess.extract(target, downloadBinaryPath); - downloadBinaryPath = fileAccess.findFirst(downloadBinaryPath, Files::isExecutable, false); + executable = fileAccess.findFirst(downloadBinaryPath, Files::isExecutable, false); } ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.WARNING).executable(executable); int exitCode = pc.run(); diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/PackageManager.java b/cli/src/main/java/com/devonfw/tools/ide/tool/PackageManager.java index 9af593534..acc581753 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/PackageManager.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/PackageManager.java @@ -1,5 +1,30 @@ package com.devonfw.tools.ide.tool; +/** + * Represents a package manager used for managing software packages. + */ public enum PackageManager { - APT, ZYPPER, YUM, DNF + APT, ZYPPER, YUM, DNF; + + /** + * Extracts the package manager from the provided command string. + * + * @param command The command string to extract the package manager from. + * @return The corresponding {@code PackageManager} based on the provided command string. + * @throws IllegalArgumentException If the command string does not contain a recognized package manager. + */ + public static PackageManager extractPackageManager(String command) { + + if (command.contains("apt")) return APT; + if (command.contains("yum")) return YUM; + if (command.contains("zypper")) return ZYPPER; + if (command.contains("dnf")) return DNF; + + throw new IllegalArgumentException("Unknown package manager in command: " + command); + } + + public String getBinaryName() { + + return name().toLowerCase(); + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/PackageManagerCommand.java b/cli/src/main/java/com/devonfw/tools/ide/tool/PackageManagerCommand.java new file mode 100644 index 000000000..d39516ac9 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/PackageManagerCommand.java @@ -0,0 +1,26 @@ +package com.devonfw.tools.ide.tool; + +import java.util.List; + +/** + * Represents a command to be executed by a package manager. Each command consists of a {@link PackageManager} and a + * list of commands to be executed by that package manager. + * + * @param packageManager The package manager associated with this command. + * @param commands The list of commands to be executed by the package manager. + */ +public record PackageManagerCommand(PackageManager packageManager, List commands) { + + /** + * Constructs a {@code PackageManagerCommand} based on the provided command string. The package manager is retrieved + * from the command string using {@link PackageManager#extractPackageManager(String)}. + * + * @param command The command string. + * @return A {@code PackageManagerCommand} based on the provided command string. + */ + public static PackageManagerCommand of(String command) { + + PackageManager pm = PackageManager.extractPackageManager(command); + return new PackageManagerCommand(pm, List.of(command)); + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/docker/Docker.java b/cli/src/main/java/com/devonfw/tools/ide/tool/docker/Docker.java index 7de426203..f0f577b94 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/docker/Docker.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/docker/Docker.java @@ -1,9 +1,8 @@ package com.devonfw.tools.ide.tool.docker; +import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Set; import com.devonfw.tools.ide.common.Tag; @@ -12,6 +11,7 @@ import com.devonfw.tools.ide.repo.ToolRepository; import com.devonfw.tools.ide.tool.GlobalToolCommandlet; import com.devonfw.tools.ide.tool.PackageManager; +import com.devonfw.tools.ide.tool.PackageManagerCommand; import com.devonfw.tools.ide.version.VersionIdentifier; /** @@ -44,32 +44,33 @@ public boolean isExtract() { protected boolean doInstall(boolean silent) { if (this.context.getSystemInfo().isLinux()) { - return installWithPackageManger(getPackageMangerCommands(), silent); + return installWithPackageManager(silent, getPackageManagerCommands()); } else { return super.doInstall(silent); } } - private Map> getPackageMangerCommands() { - - Map> commands = new HashMap<>(); + private List getPackageManagerCommands() { String edition = getEdition(); ToolRepository toolRepository = this.context.getDefaultToolRepository(); VersionIdentifier configuredVersion = getConfiguredVersion(); String resolvedVersion = toolRepository.resolveVersion(this.tool, edition, configuredVersion).toString(); - commands.put(PackageManager.ZYPPER, Arrays.asList( + List pmCommands = new ArrayList<>(); + pmCommands.add(new PackageManagerCommand(PackageManager.ZYPPER, Arrays.asList( "sudo zypper addrepo https://download.opensuse.org/repositories/isv:/Rancher:/stable/rpm/isv:Rancher:stable.repo", - String.format("sudo zypper --no-gpg-checks install rancher-desktop=%s*", resolvedVersion))); - - commands.put(PackageManager.APT, Arrays.asList( - "curl -s https://download.opensuse.org/repositories/isv:/Rancher:/stable/deb/Release.key | gpg --dearmor | sudo dd status=none of=/usr/share/keyrings/isv-rancher-stable-archive-keyring.gpg", - "echo 'deb [signed-by=/usr/share/keyrings/isv-rancher-stable-archive-keyring.gpg] https://download.opensuse.org/repositories/isv:/Rancher:/stable/deb/ ./' | sudo dd status=none of=/etc/apt/sources.list.d/isv-rancher-stable.list", + String.format("sudo zypper --no-gpg-checks install rancher-desktop=%s*", resolvedVersion)))); + pmCommands.add(new PackageManagerCommand(PackageManager.APT, Arrays.asList( + "curl -s https://download.opensuse.org/repositories/isv:/Rancher:/stable/deb/Release.key | gpg --dearmor |" + + " sudo dd status=none of=/usr/share/keyrings/isv-rancher-stable-archive-keyring.gpg", + "echo 'deb [signed-by=/usr/share/keyrings/isv-rancher-stable-archive-keyring.gpg]" + + " https://download.opensuse.org/repositories/isv:/Rancher:/stable/deb/ ./' |" + + " sudo dd status=none of=/etc/apt/sources.list.d/isv-rancher-stable.list", "sudo apt update", - String.format("sudo apt install -y --allow-downgrades rancher-desktop=%s*", resolvedVersion))); + String.format("sudo apt install -y --allow-downgrades rancher-desktop=%s*", resolvedVersion)))); - return commands; + return pmCommands; } @Override