Skip to content

Commit f23348b

Browse files
authored
#256: improve package manager API (#274)
1 parent 2ff8bc3 commit f23348b

File tree

7 files changed

+210
-149
lines changed

7 files changed

+210
-149
lines changed

cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java

+75-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
package com.devonfw.tools.ide.context;
22

3+
import java.io.BufferedReader;
4+
import java.io.IOException;
5+
import java.io.InputStreamReader;
6+
import java.net.InetAddress;
7+
import java.nio.file.Files;
8+
import java.nio.file.Path;
9+
import java.util.HashMap;
10+
import java.util.Iterator;
11+
import java.util.List;
12+
import java.util.Locale;
13+
import java.util.Map;
14+
import java.util.Objects;
15+
import java.util.function.Function;
16+
317
import com.devonfw.tools.ide.cli.CliAbortException;
418
import com.devonfw.tools.ide.cli.CliArgument;
519
import com.devonfw.tools.ide.cli.CliArguments;
@@ -36,18 +50,6 @@
3650
import com.devonfw.tools.ide.step.StepImpl;
3751
import com.devonfw.tools.ide.url.model.UrlMetadata;
3852

39-
import java.io.IOException;
40-
import java.net.InetAddress;
41-
import java.nio.file.Files;
42-
import java.nio.file.Path;
43-
import java.util.HashMap;
44-
import java.util.Iterator;
45-
import java.util.List;
46-
import java.util.Locale;
47-
import java.util.Map;
48-
import java.util.Objects;
49-
import java.util.function.Function;
50-
5153
/**
5254
* Abstract base implementation of {@link IdeContext}.
5355
*/
@@ -929,4 +931,65 @@ public boolean apply(CliArguments arguments, Commandlet cmd, CompletionCandidate
929931
return true;
930932
}
931933

934+
public String findBash() {
935+
936+
String bash = "bash";
937+
if (SystemInfoImpl.INSTANCE.isWindows()) {
938+
bash = findBashOnWindows();
939+
}
940+
941+
return bash;
942+
}
943+
944+
private String findBashOnWindows() {
945+
946+
// Check if Git Bash exists in the default location
947+
Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
948+
if (Files.exists(defaultPath)) {
949+
return defaultPath.toString();
950+
}
951+
952+
// If not found in the default location, try the registry query
953+
String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
954+
String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
955+
String regQueryResult;
956+
for (String bashVariant : bashVariants) {
957+
for (String registryKey : registryKeys) {
958+
String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
959+
String command = "reg query " + registryKey + "\\Software\\" + bashVariant + " /v " + toolValueName + " 2>nul";
960+
961+
try {
962+
Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
963+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
964+
StringBuilder output = new StringBuilder();
965+
String line;
966+
967+
while ((line = reader.readLine()) != null) {
968+
output.append(line);
969+
}
970+
971+
int exitCode = process.waitFor();
972+
if (exitCode != 0) {
973+
return null;
974+
}
975+
976+
regQueryResult = output.toString();
977+
if (regQueryResult != null) {
978+
int index = regQueryResult.indexOf("REG_SZ");
979+
if (index != -1) {
980+
String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
981+
return path + "\\bin\\bash.exe";
982+
}
983+
}
984+
985+
}
986+
} catch (Exception e) {
987+
return null;
988+
}
989+
}
990+
}
991+
// no bash found
992+
throw new IllegalStateException("Could not find Bash. Please install Git for Windows and rerun.");
993+
}
994+
932995
}

cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java

+8
Original file line numberDiff line numberDiff line change
@@ -471,4 +471,12 @@ default void setIdeHome(Path ideHome) {
471471
* @param ideHome The path to the IDE home directory.
472472
*/
473473
void setCwd(Path userDir, String workspace, Path ideHome);
474+
475+
/**
476+
* Finds the path to the Bash executable.
477+
*
478+
* @return the {@link String} to the Bash executable, or {@code null} if Bash is not found
479+
*/
480+
String findBash();
481+
474482
}

cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java

+12-80
Original file line numberDiff line numberDiff line change
@@ -245,57 +245,6 @@ private String getSheBang(Path file) {
245245
return null;
246246
}
247247

248-
private String findBashOnWindows() {
249-
250-
// Check if Git Bash exists in the default location
251-
Path defaultPath = Path.of("C:\\Program Files\\Git\\bin\\bash.exe");
252-
if (Files.exists(defaultPath)) {
253-
return defaultPath.toString();
254-
}
255-
256-
// If not found in the default location, try the registry query
257-
String[] bashVariants = { "GitForWindows", "Cygwin\\setup" };
258-
String[] registryKeys = { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER" };
259-
String regQueryResult;
260-
for (String bashVariant : bashVariants) {
261-
for (String registryKey : registryKeys) {
262-
String toolValueName = ("GitForWindows".equals(bashVariant)) ? "InstallPath" : "rootdir";
263-
String command = "reg query " + registryKey + "\\Software\\" + bashVariant + " /v " + toolValueName + " 2>nul";
264-
265-
try {
266-
Process process = new ProcessBuilder("cmd.exe", "/c", command).start();
267-
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
268-
StringBuilder output = new StringBuilder();
269-
String line;
270-
271-
while ((line = reader.readLine()) != null) {
272-
output.append(line);
273-
}
274-
275-
int exitCode = process.waitFor();
276-
if (exitCode != 0) {
277-
return null;
278-
}
279-
280-
regQueryResult = output.toString();
281-
if (regQueryResult != null) {
282-
int index = regQueryResult.indexOf("REG_SZ");
283-
if (index != -1) {
284-
String path = regQueryResult.substring(index + "REG_SZ".length()).trim();
285-
return path + "\\bin\\bash.exe";
286-
}
287-
}
288-
289-
}
290-
} catch (Exception e) {
291-
return null;
292-
}
293-
}
294-
}
295-
// no bash found
296-
throw new IllegalStateException("Could not find Bash. Please install Git for Windows and rerun.");
297-
}
298-
299248
private String addExecutable(String exec, List<String> args) {
300249

301250
String interpreter = null;
@@ -317,19 +266,12 @@ private String addExecutable(String exec, List<String> args) {
317266
}
318267
}
319268
if (isBashScript) {
320-
String bash = "bash";
321-
interpreter = bash;
322-
// here we want to have native OS behavior even if OS is mocked during tests...
323-
if (SystemInfoImpl.INSTANCE.isWindows()) {
324-
String findBashOnWindowsResult = findBashOnWindows();
325-
if (findBashOnWindowsResult != null) {
326-
bash = findBashOnWindowsResult;
327-
}
328-
}
329-
args.add(bash);
330-
} else if (SystemInfoImpl.INSTANCE.isWindows() && "msi".equalsIgnoreCase(fileExtension)) {
331-
args.add("msiexec");
332-
args.add("/i");
269+
interpreter = "bash";
270+
args.add(this.context.findBash());
271+
}
272+
if ("msi".equalsIgnoreCase(fileExtension)) {
273+
args.add(0, "/i");
274+
args.add(0, "msiexec");
333275
}
334276
args.add(exec);
335277
return interpreter;
@@ -365,22 +307,12 @@ private void modifyArgumentsOnBackgroundProcess(ProcessMode processMode) {
365307
throw new IllegalStateException("Cannot handle non background process mode!");
366308
}
367309

368-
String bash = "bash";
369-
370-
// try to use bash in windows to start the process
371-
if (this.context.getSystemInfo().isWindows()) {
372-
373-
String findBashOnWindowsResult = findBashOnWindows();
374-
if (findBashOnWindowsResult != null) {
375-
376-
bash = findBashOnWindowsResult;
377-
378-
} else {
379-
this.context.warning(
380-
"Cannot start background process in windows! No bash installation found, output will be discarded.");
381-
this.processBuilder.redirectOutput(Redirect.DISCARD).redirectError(Redirect.DISCARD);
382-
return;
383-
}
310+
String bash = this.context.findBash();
311+
if (bash == null) {
312+
context.warning(
313+
"Cannot start background process via bash because no bash installation was found. Hence, output will be discarded.");
314+
this.processBuilder.redirectOutput(Redirect.DISCARD).redirectError(Redirect.DISCARD);
315+
return;
384316
}
385317

386318
String commandToRunInBackground = buildCommandToRunInBackground();

cli/src/main/java/com/devonfw/tools/ide/tool/GlobalToolCommandlet.java

+48-42
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import java.nio.file.Files;
44
import java.nio.file.Path;
5+
import java.util.Arrays;
56
import java.util.List;
6-
import java.util.Map;
77
import java.util.Set;
88

99
import com.devonfw.tools.ide.common.Tag;
@@ -26,7 +26,7 @@ public abstract class GlobalToolCommandlet extends ToolCommandlet {
2626
* @param context the {@link IdeContext}.
2727
* @param tool the {@link #getName() tool name}.
2828
* @param tags the {@link #getTags() tags} classifying the tool. Should be created via {@link Set#of(Object) Set.of}
29-
* method.
29+
* method.
3030
*/
3131
public GlobalToolCommandlet(IdeContext context, String tool, Set<Tag> tags) {
3232

@@ -36,57 +36,63 @@ public GlobalToolCommandlet(IdeContext context, String tool, Set<Tag> tags) {
3636
/**
3737
* Performs the installation of the {@link #getName() tool} via a package manager.
3838
*
39-
* @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
40-
* @param commands - A {@link Map} containing the commands used to perform the installation for each package manager.
41-
* @return {@code true} if the tool was newly installed, {@code false} if the tool was already installed before and
42-
* nothing has changed.
39+
* @param silent {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
40+
* @param commandStrings commandStrings The package manager command strings to execute.
41+
* @return {@code true} if installation succeeds with any of the package manager commands, {@code false} otherwise.
4342
*/
44-
protected boolean installWithPackageManger(Map<PackageManager, List<String>> commands, boolean silent) {
43+
protected boolean installWithPackageManager(boolean silent, String... commandStrings) {
4544

46-
Path binaryPath = this.context.getPath().findBinary(Path.of(getBinaryName()));
47-
48-
if (binaryPath != null && Files.exists(binaryPath) && !this.context.isForceMode()) {
49-
IdeLogLevel level = silent ? IdeLogLevel.DEBUG : IdeLogLevel.INFO;
50-
this.context.level(level).log("{} is already installed at {}", this.tool, binaryPath);
51-
return false;
52-
}
45+
List<PackageManagerCommand> pmCommands = Arrays.stream(commandStrings).map(PackageManagerCommand::of).toList();
46+
return installWithPackageManager(silent, pmCommands);
47+
}
5348

54-
Path bashPath = this.context.getPath().findBinary(Path.of("bash"));
55-
if (bashPath == null || !Files.exists(bashPath)) {
56-
context.warning("Bash was not found on this machine. Not Proceeding with installation of tool " + this.tool);
57-
return false;
58-
}
49+
/**
50+
* Performs the installation of the {@link #getName() tool} via a package manager.
51+
*
52+
* @param silent {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
53+
* @param pmCommands A list of {@link PackageManagerCommand} to be used for installation.
54+
* @return {@code true} if installation succeeds with any of the package manager commands, {@code false} otherwise.
55+
*/
56+
protected boolean installWithPackageManager(boolean silent, List<PackageManagerCommand> pmCommands) {
57+
58+
for (PackageManagerCommand pmCommand : pmCommands) {
59+
PackageManager packageManager = pmCommand.packageManager();
60+
Path packageManagerPath = this.context.getPath().findBinary(Path.of(packageManager.getBinaryName()));
61+
if (packageManagerPath == null || !Files.exists(packageManagerPath)) {
62+
this.context.debug("{} is not installed", packageManager.toString());
63+
continue; // Skip to the next package manager command
64+
}
5965

60-
PackageManager foundPackageManager = null;
61-
for (PackageManager pm : commands.keySet()) {
62-
if (Files.exists(this.context.getPath().findBinary(Path.of(pm.toString().toLowerCase())))) {
63-
foundPackageManager = pm;
64-
break;
66+
if (executePackageManagerCommand(pmCommand, silent)) {
67+
return true; // Successfully installed
6568
}
6669
}
70+
return false; // None of the package manager commands were successful
71+
}
6772

68-
int finalExitCode = 0;
69-
if (foundPackageManager == null) {
70-
context.warning("No supported Package Manager found for installation");
71-
return false;
72-
} else {
73-
List<String> commandList = commands.get(foundPackageManager);
74-
if (commandList != null) {
75-
for (String command : commandList) {
76-
ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.WARNING).executable(bashPath)
77-
.addArgs("-c", command);
78-
finalExitCode = pc.run();
79-
}
73+
/**
74+
* Executes the provided package manager command.
75+
*
76+
* @param pmCommand The {@link PackageManagerCommand} containing the commands to execute.
77+
* @param silent {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
78+
* @return {@code true} if the package manager commands execute successfully, {@code false} otherwise.
79+
*/
80+
private boolean executePackageManagerCommand(PackageManagerCommand pmCommand, boolean silent) {
81+
82+
String bashPath = this.context.findBash();
83+
for (String command : pmCommand.commands()) {
84+
ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.WARNING).executable(bashPath)
85+
.addArgs("-c", command);
86+
int exitCode = pc.run();
87+
if (exitCode != 0) {
88+
this.context.warning("{} command did not execute successfully", command);
89+
return false;
8090
}
8191
}
8292

83-
if (finalExitCode == 0) {
93+
if (!silent) {
8494
this.context.success("Successfully installed {}", this.tool);
85-
} else {
86-
this.context.warning("{} was not successfully installed", this.tool);
87-
return false;
8895
}
89-
postInstall();
9096
return true;
9197
}
9298

@@ -122,7 +128,7 @@ protected boolean doInstall(boolean silent) {
122128
tmpDir = fileAccess.createTempDir(getName());
123129
Path downloadBinaryPath = tmpDir.resolve(target.getFileName());
124130
fileAccess.extract(target, downloadBinaryPath);
125-
downloadBinaryPath = fileAccess.findFirst(downloadBinaryPath, Files::isExecutable, false);
131+
executable = fileAccess.findFirst(downloadBinaryPath, Files::isExecutable, false);
126132
}
127133
ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.WARNING).executable(executable);
128134
int exitCode = pc.run();
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,30 @@
11
package com.devonfw.tools.ide.tool;
22

3+
/**
4+
* Represents a package manager used for managing software packages.
5+
*/
36
public enum PackageManager {
4-
APT, ZYPPER, YUM, DNF
7+
APT, ZYPPER, YUM, DNF;
8+
9+
/**
10+
* Extracts the package manager from the provided command string.
11+
*
12+
* @param command The command string to extract the package manager from.
13+
* @return The corresponding {@code PackageManager} based on the provided command string.
14+
* @throws IllegalArgumentException If the command string does not contain a recognized package manager.
15+
*/
16+
public static PackageManager extractPackageManager(String command) {
17+
18+
if (command.contains("apt")) return APT;
19+
if (command.contains("yum")) return YUM;
20+
if (command.contains("zypper")) return ZYPPER;
21+
if (command.contains("dnf")) return DNF;
22+
23+
throw new IllegalArgumentException("Unknown package manager in command: " + command);
24+
}
25+
26+
public String getBinaryName() {
27+
28+
return name().toLowerCase();
29+
}
530
}

0 commit comments

Comments
 (0)