Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#256: improve package manager API #274

Merged
merged 17 commits into from
Apr 19, 2024
Merged
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
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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}.
*/
Expand Down Expand Up @@ -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.");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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();

}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> args) {

String interpreter = null;
Expand All @@ -317,19 +266,12 @@ private String addExecutable(String exec, List<String> 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;
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Tag> tags) {

Expand All @@ -36,57 +36,63 @@ public GlobalToolCommandlet(IdeContext context, String tool, Set<Tag> 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<PackageManager, List<String>> 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<PackageManagerCommand> 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<PackageManagerCommand> 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<String> 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;
}

Expand Down Expand Up @@ -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();
Expand Down
27 changes: 26 additions & 1 deletion cli/src/main/java/com/devonfw/tools/ide/tool/PackageManager.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading
Loading