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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Release with new features and bugfixes:
* https://github.com/devonfw/IDEasy/issues/1978[#1978]: Enhanced the quality status documentation
* https://github.com/devonfw/IDEasy/issues/1946[#1946]: Add Spyder Python IDE
* https://github.com/devonfw/IDEasy/issues/1846[#1846]: Fixed macOS Gatekeeper workaround
* https://github.com/devonfw/IDEasy/issues/798[#798]: Fixed pgAdmin DMG install on macOS
* https://github.com/devonfw/IDEasy/issues/1906[#1906]: Fixed `ide uninstall` failing on macOS
* https://github.com/devonfw/IDEasy/issues/1985[#1985]: Fix path traversal errors when extracting ZIPs on Windows
* https://github.com/devonfw/IDEasy/issues/1255[#1255]: Enhance snapshot version recognition in IDEasy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ public interface IdeContext extends IdeStartContext {
/** The name of the Contents folder inside a MacOS app. */
String FOLDER_CONTENTS = "Contents";

/** The name of the MacOS folder inside the Contents of a MacOS app, holding the executable. */
String FOLDER_MAC_OS = "MacOS";

/** The name of the Resources folder inside a MacOS app. */
String FOLDER_RESOURCES = "Resources";

Expand Down
34 changes: 23 additions & 11 deletions cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import com.devonfw.tools.ide.io.ini.IniSection;
import com.devonfw.tools.ide.os.SystemInfoImpl;
import com.devonfw.tools.ide.process.ProcessContext;
import com.devonfw.tools.ide.process.ProcessErrorHandling;
import com.devonfw.tools.ide.process.ProcessMode;
import com.devonfw.tools.ide.process.ProcessResult;
import com.devonfw.tools.ide.util.DateTimeUtil;
Expand Down Expand Up @@ -1000,18 +1001,29 @@ public void extractDmg(Path file, Path targetDir) {

Path mountPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_UPDATES).resolve(IdeContext.FOLDER_VOLUME);
mkdirs(mountPath);
ProcessContext pc = this.context.newProcess();
pc.executable("hdiutil");
pc.addArgs("attach", "-quiet", "-nobrowse", "-mountpoint", mountPath, file);
pc.run();
Path appPath = findFirst(mountPath, p -> p.getFileName().toString().endsWith(".app"), false);
if (appPath == null) {
throw new IllegalStateException("Failed to unpack DMG as no MacOS *.app was found in file " + file);
}
boolean mounted = false;
try {
ProcessContext pc = this.context.newProcess();
pc.executable("hdiutil");
pc.addArgs("attach", "-quiet", "-nobrowse", "-mountpoint", mountPath, file);
pc.run();
mounted = true;
Path appPath = findFirst(mountPath, p -> p.getFileName().toString().endsWith(".app"), false);
if (appPath == null) {
throw new IllegalStateException("Failed to unpack DMG as no MacOS *.app was found in file " + file);
}

copy(appPath, targetDir, FileCopyMode.COPY_TREE_OVERRIDE_TREE);
pc.addArgs("detach", "-force", mountPath);
pc.run();
// use ditto to copy the .app: a normal recursive copy breaks on the symlinks inside macOS frameworks (e.g. Python/Tcl)
Path targetApp = targetDir.resolve(appPath.getFileName().toString());
mkdirs(targetDir);
delete(targetApp);
this.context.newProcess().executable("ditto").addArgs(appPath, targetApp).run();
} finally {
if (mounted) {
this.context.newProcess().errorHandling(ProcessErrorHandling.LOG_WARNING).executable("hdiutil").addArgs("detach", "-force", mountPath)
.run(ProcessMode.DEFAULT_SILENT);
}
}
}

@Override
Expand Down
154 changes: 130 additions & 24 deletions cli/src/main/java/com/devonfw/tools/ide/tool/GlobalToolCommandlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.devonfw.tools.ide.process.ProcessMode;
import com.devonfw.tools.ide.step.Step;
import com.devonfw.tools.ide.tool.repository.ToolRepository;
import com.devonfw.tools.ide.util.FilenameUtil;
import com.devonfw.tools.ide.version.VersionIdentifier;

/**
Expand Down Expand Up @@ -77,16 +78,26 @@ protected boolean runWithPackageManager(boolean silent, List<PackageManagerComma
return false; // None of the package manager commands were successful
}

private void logPackageManagerCommands(PackageManagerCommand pmCommand) {
/**
* Logs the privileged commands before execution so the user knows why sudo/root permissions are requested.
*
* @param commands the privileged commands to log.
*/
protected void logPrivilegedCommands(List<String> commands) {

IdeLogLevel level = IdeLogLevel.INTERACTION;
level.log(LOG, "We need to run the following privileged command(s):");
for (String command : pmCommand.commands()) {
for (String command : commands) {
level.log(LOG, command);
}
level.log(LOG, "This will require root permissions!");
}

private void logPackageManagerCommands(PackageManagerCommand pmCommand) {

logPrivilegedCommands(pmCommand.commands());
}

/**
* Executes the provided package manager command.
*
Expand Down Expand Up @@ -147,30 +158,35 @@ protected ToolInstallation doInstall(ToolInstallRequest request) {
ToolRepository toolRepository = this.context.getDefaultToolRepository();
resolvedVersion = cveCheck(request);
// download and install the global tool
FileAccess fileAccess = this.context.getFileAccess();
Path target = toolRepository.download(this.tool, edition, resolvedVersion, this);
Path executable = target;
Path tmpDir = null;
boolean extract = isExtract();
if (extract) {
tmpDir = fileAccess.createTempDir(getName());
Path downloadBinaryPath = tmpDir.resolve(target.getFileName());
fileAccess.extract(target, downloadBinaryPath);
executable = fileAccess.findFirst(downloadBinaryPath, Files::isExecutable, false);
}
ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.LOG_WARNING).executable(executable);
int exitCode = pc.run(ProcessMode.BACKGROUND_SILENT).getExitCode();
if (tmpDir != null) {
fileAccess.delete(tmpDir);
}
if (exitCode == 0) {
IdeLogLevel.SUCCESS.log(LOG, "Installation process for {} in version {} has started", this.tool, resolvedVersion);
Step step = request.getStep();
if (step != null) {
step.success(true);
}
ProcessContext pc;
if (isMacDmg(target)) {
installMacDmg(target);
pc = request.getProcessContext();
} else {
throw new CliException("Installation process for " + this.tool + " in version " + resolvedVersion + " failed with exit code " + exitCode + "!");
FileAccess fileAccess = this.context.getFileAccess();
Path executable = target;
Path tmpDir = null;
boolean extract = isExtract();
if (extract) {
tmpDir = fileAccess.createTempDir(getName());
Path downloadBinaryPath = tmpDir.resolve(target.getFileName());
fileAccess.extract(target, downloadBinaryPath);
executable = fileAccess.findFirst(downloadBinaryPath, Files::isExecutable, false);
}
pc = this.context.newProcess().errorHandling(ProcessErrorHandling.LOG_WARNING).executable(executable);
int exitCode = pc.run(ProcessMode.BACKGROUND_SILENT).getExitCode();
if (tmpDir != null) {
fileAccess.delete(tmpDir);
}
if (exitCode != 0) {
throw new CliException("Installation process for " + this.tool + " in version " + resolvedVersion + " failed with exit code " + exitCode + "!");
}
}
IdeLogLevel.SUCCESS.log(LOG, "Installation process for {} in version {} has started", this.tool, resolvedVersion);
Step step = request.getStep();
if (step != null) {
step.success(true);
}
installationPath = getInstallationPath(toolEdition.edition(), resolvedVersion);
if (installationPath == null) {
Expand All @@ -180,6 +196,69 @@ protected ToolInstallation doInstall(ToolInstallRequest request) {
return createToolInstallation(installationPath, resolvedVersion, true, pc, false);
}

private void installMacDmg(Path downloadedToolFile) {

FileAccess fileAccess = this.context.getFileAccess();
Path tmpDir = fileAccess.createTempDir(getName());
try {
fileAccess.extractDmg(downloadedToolFile, tmpDir);
Path sourceApp = getMacOsHelper().findAppDir(tmpDir);
if (sourceApp == null) {
throw new CliException("Failed to install " + this.tool + " from " + downloadedToolFile + " because no MacOS *.app was found.");
}
Path targetApp = getMacApplicationsPath().resolve(sourceApp.getFileName().toString());
copyMacApplicationToApplications(sourceApp, targetApp);
} finally {
fileAccess.delete(tmpDir);
}
}

/**
* Copies a macOS application bundle to the global applications folder.
*
* @param sourceApp the extracted source {@code .app}.
* @param targetApp the target {@code .app} in {@link #getMacApplicationsPath()}.
*/
protected void copyMacApplicationToApplications(Path sourceApp, Path targetApp) {

runPrivilegedCommands(List.of(
List.of("/bin/rm", "-rf", targetApp.toString()),
List.of("/usr/bin/ditto", sourceApp.toString(), targetApp.toString())));
}

private void runPrivilegedCommands(List<List<String>> commands) {

logPrivilegedCommands(commands.stream().map(this::toSudoCommandLine).toList());
for (List<String> command : commands) {
int exitCode = this.context.newProcess().errorHandling(ProcessErrorHandling.LOG_WARNING).executable("sudo").addArgs(command).run();
if (exitCode != 0) {
throw new CliException("Privileged command failed with exit code " + exitCode + ": " + toSudoCommandLine(command));
}
}
}

private String toSudoCommandLine(List<String> command) {

return "sudo " + String.join(" ", command);
}

private boolean isMacDmg(Path file) {

if (!this.context.getSystemInfo().isMac()) {
return false;
}
String extension = FilenameUtil.getExtension(file.toString());
return "dmg".equals(extension);
}

/**
* @return the macOS applications folder where global {@code .dmg} tools are installed.
*/
protected Path getMacApplicationsPath() {

return Path.of("/Applications");
}

/**
* @return the {@link List} of {@link PackageManagerCommand}s to use on Linux to install this tool. If empty, no package manager installation will be
* triggered on Linux.
Expand All @@ -206,6 +285,9 @@ protected Path getInstallationPath(String edition, VersionIdentifier resolvedVer
Path toolBinary = Path.of(getBinaryName());
Path binaryPath = this.context.getPath().findBinary(toolBinary);
if ((binaryPath == toolBinary) || !Files.exists(binaryPath)) {
if (this.context.getSystemInfo().isMac()) {
return getMacApplicationInstallationPath();
}
return null;
}
Path binPath = binaryPath.getParent();
Expand All @@ -215,6 +297,30 @@ protected Path getInstallationPath(String edition, VersionIdentifier resolvedVer
return this.context.getFileAccess().getBinParentPath(binPath);
}

private Path getMacApplicationInstallationPath() {

Path appPath = this.context.getFileAccess().findFirst(getMacApplicationsPath(), this::isMacApplicationForTool, false);
if (appPath == null) {
return null;
}
Path binaryPath = getMacApplicationBinaryPath(appPath);
this.context.getPath().setPath(getName(), binaryPath.getParent());
return appPath;
}

private boolean isMacApplicationForTool(Path appPath) {

if (!Files.isDirectory(appPath) || !appPath.getFileName().toString().endsWith(".app")) {
return false;
}
return Files.isExecutable(getMacApplicationBinaryPath(appPath));
}

private Path getMacApplicationBinaryPath(Path appPath) {

return appPath.resolve(IdeContext.FOLDER_CONTENTS).resolve(IdeContext.FOLDER_MAC_OS).resolve(getBinaryName());
}

@Override
public void uninstall() {
//TODO: handle "uninstall <globaltool>"
Expand Down
16 changes: 12 additions & 4 deletions cli/src/main/java/com/devonfw/tools/ide/tool/pgadmin/PgAdmin.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
*/
public class PgAdmin extends GlobalToolCommandlet {

private static final String PGADMIN_MAC_BINARY = "pgAdmin 4";

/**
* The constructor.
*
Expand Down Expand Up @@ -74,15 +76,21 @@ private List<PackageManagerCommand> getPackageManagerCommandsUninstall() {
@Override
protected String getBinaryName() {

if (this.context.getSystemInfo().isMac()) {
return PGADMIN_MAC_BINARY;
}
return "pgadmin4";
}

@Override
protected Path getInstallationPath(String edition, VersionIdentifier resolvedVersion) {
if (super.getInstallationPath(edition, resolvedVersion) == null) {
if (this.context.getSystemInfo().isWindows()) {
return getExecutableFolderFromWindowsRegistry();
}

Path installationPath = super.getInstallationPath(edition, resolvedVersion);
if (installationPath != null) {
return installationPath;
}
if (this.context.getSystemInfo().isWindows()) {
return getExecutableFolderFromWindowsRegistry();
}
return null;
}
Expand Down
Loading
Loading