• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

devonfw / IDEasy / 27652215079

16 Jun 2026 10:26PM UTC coverage: 71.202% (-0.08%) from 71.285%
27652215079

Pull #2018

github

web-flow
Merge fa9133a8c into ec9c73a45
Pull Request #2018: fixed pgAdmin macOS dmg installation

4688 of 7272 branches covered (64.47%)

Branch coverage included in aggregate %.

12078 of 16275 relevant lines covered (74.21%)

3.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

15.34
cli/src/main/java/com/devonfw/tools/ide/tool/GlobalToolCommandlet.java
1
package com.devonfw.tools.ide.tool;
2

3
import java.nio.file.Files;
4
import java.nio.file.Path;
5
import java.util.Arrays;
6
import java.util.List;
7
import java.util.Set;
8

9
import org.slf4j.Logger;
10
import org.slf4j.LoggerFactory;
11

12
import com.devonfw.tools.ide.cli.CliException;
13
import com.devonfw.tools.ide.common.Tag;
14
import com.devonfw.tools.ide.context.IdeContext;
15
import com.devonfw.tools.ide.io.FileAccess;
16
import com.devonfw.tools.ide.log.IdeLogLevel;
17
import com.devonfw.tools.ide.process.ProcessContext;
18
import com.devonfw.tools.ide.process.ProcessErrorHandling;
19
import com.devonfw.tools.ide.process.ProcessMode;
20
import com.devonfw.tools.ide.step.Step;
21
import com.devonfw.tools.ide.tool.repository.ToolRepository;
22
import com.devonfw.tools.ide.util.FilenameUtil;
23
import com.devonfw.tools.ide.version.VersionIdentifier;
24

25
/**
26
 * {@link ToolCommandlet} that is installed globally.
27
 */
28
public abstract class GlobalToolCommandlet extends ToolCommandlet {
29

30
  private static final Logger LOG = LoggerFactory.getLogger(GlobalToolCommandlet.class);
4✔
31

32
  /**
33
   * The constructor.
34
   *
35
   * @param context the {@link IdeContext}.
36
   * @param tool the {@link #getName() tool name}.
37
   * @param tags the {@link #getTags() tags} classifying the tool. Should be created via {@link Set#of(Object) Set.of} method.
38
   */
39
  public GlobalToolCommandlet(IdeContext context, String tool, Set<Tag> tags) {
40

41
    super(context, tool, tags);
5✔
42
  }
1✔
43

44
  /**
45
   * Performs the installation or uninstallation of the {@link #getName() tool} via a package manager.
46
   *
47
   * @param silent {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
48
   * @param commandStrings commandStrings The package manager command strings to execute.
49
   * @return {@code true} if installation or uninstallation succeeds with any of the package manager commands, {@code false} otherwise.
50
   */
51
  protected boolean runWithPackageManager(boolean silent, String... commandStrings) {
52

53
    List<PackageManagerCommand> pmCommands = Arrays.stream(commandStrings).map(PackageManagerCommand::of).toList();
×
54
    return runWithPackageManager(silent, pmCommands);
×
55
  }
56

57
  /**
58
   * Performs the installation or uninstallation of the {@link #getName() tool} via a package manager.
59
   *
60
   * @param silent {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
61
   * @param pmCommands A list of {@link PackageManagerCommand} to be used for installation or uninstallation.
62
   * @return {@code true} if installation or uninstallation succeeds with any of the package manager commands, {@code false} otherwise.
63
   */
64
  protected boolean runWithPackageManager(boolean silent, List<PackageManagerCommand> pmCommands) {
65

66
    for (PackageManagerCommand pmCommand : pmCommands) {
×
67
      NativePackageManager packageManager = pmCommand.packageManager();
×
68
      Path packageManagerPath = this.context.getPath().findBinary(Path.of(packageManager.getBinaryName()));
×
69
      if (packageManagerPath == null || !Files.exists(packageManagerPath)) {
×
70
        LOG.debug("{} is not installed", packageManager.toString());
×
71
        continue; // Skip to the next package manager command
×
72
      }
73

74
      if (executePackageManagerCommand(pmCommand, silent)) {
×
75
        return true; // Success
×
76
      }
77
    }
×
78
    return false; // None of the package manager commands were successful
×
79
  }
80

81
  /**
82
   * Logs the privileged commands before execution so the user knows why sudo/root permissions are requested.
83
   *
84
   * @param commands the privileged commands to log.
85
   */
86
  protected void logPrivilegedCommands(List<String> commands) {
87

88
    IdeLogLevel level = IdeLogLevel.INTERACTION;
×
89
    level.log(LOG, "We need to run the following privileged command(s):");
×
90
    for (String command : commands) {
×
91
      level.log(LOG, command);
×
92
    }
×
93
    level.log(LOG, "This will require root permissions!");
×
94
  }
×
95

96
  private void logPackageManagerCommands(PackageManagerCommand pmCommand) {
97

98
    logPrivilegedCommands(pmCommand.commands());
×
99
  }
×
100

101
  /**
102
   * Executes the provided package manager command.
103
   *
104
   * @param pmCommand The {@link PackageManagerCommand} containing the commands to execute.
105
   * @param silent {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
106
   * @return {@code true} if the package manager commands execute successfully, {@code false} otherwise.
107
   */
108
  private boolean executePackageManagerCommand(PackageManagerCommand pmCommand, boolean silent) {
109

110
    String bashPath = this.context.findBashRequired().toString();
×
111
    logPackageManagerCommands(pmCommand);
×
112
    for (String command : pmCommand.commands()) {
×
113
      ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.LOG_WARNING).executable(bashPath)
×
114
          .addArgs("-c", command);
×
115
      int exitCode = pc.run();
×
116
      if (exitCode != 0) {
×
117
        LOG.warn("{} command did not execute successfully", command);
×
118
        return false;
×
119
      }
120
    }
×
121

122
    if (!silent) {
×
123
      IdeLogLevel.SUCCESS.log(LOG, "Successfully installed {}", this.tool);
×
124
    }
125
    return true;
×
126
  }
127

128
  @Override
129
  protected boolean isExtract() {
130

131
    // for global tools we usually download installers and do not want to extract them (e.g. installer.msi file shall
132
    // not be extracted)
133
    return false;
×
134
  }
135

136
  @Override
137
  protected ToolInstallation doInstall(ToolInstallRequest request) {
138

139
    VersionIdentifier resolvedVersion = request.getRequested().getResolvedVersion();
×
140
    if (this.context.getSystemInfo().isLinux()) {
×
141
      // on Linux global tools are typically installed via the package manager of the OS
142
      // if a global tool implements this method to return at least one PackageManagerCommand, then we install this way.
143
      List<PackageManagerCommand> commands = getInstallPackageManagerCommands();
×
144
      if (!commands.isEmpty()) {
×
145
        boolean newInstallation = runWithPackageManager(request.isSilent(), commands);
×
146
        Path rootDir = getInstallationPath(getConfiguredEdition(), resolvedVersion);
×
147
        return createToolInstallation(rootDir, resolvedVersion, newInstallation, request.getProcessContext(), request.isAdditionalInstallation());
×
148
      }
149
    }
150

151
    ToolEdition toolEdition = getToolWithConfiguredEdition();
×
152
    Path installationPath = getInstallationPath(toolEdition.edition(), resolvedVersion);
×
153
    // if force mode is enabled, go through with the installation even if the tool is already installed
154
    if ((installationPath != null) && !this.context.isForceMode()) {
×
155
      return toolAlreadyInstalled(request);
×
156
    }
157
    String edition = toolEdition.edition();
×
158
    ToolRepository toolRepository = this.context.getDefaultToolRepository();
×
159
    resolvedVersion = cveCheck(request);
×
160
    // download and install the global tool
161
    Path target = toolRepository.download(this.tool, edition, resolvedVersion, this);
×
162
    ProcessContext pc;
163
    if (isMacDmg(target)) {
×
164
      installMacDmg(target);
×
165
      pc = request.getProcessContext();
×
166
    } else {
167
      FileAccess fileAccess = this.context.getFileAccess();
×
168
      Path executable = target;
×
169
      Path tmpDir = null;
×
170
      boolean extract = isExtract();
×
171
      if (extract) {
×
172
        tmpDir = fileAccess.createTempDir(getName());
×
173
        Path downloadBinaryPath = tmpDir.resolve(target.getFileName());
×
174
        fileAccess.extract(target, downloadBinaryPath);
×
175
        executable = fileAccess.findFirst(downloadBinaryPath, Files::isExecutable, false);
×
176
      }
177
      pc = this.context.newProcess().errorHandling(ProcessErrorHandling.LOG_WARNING).executable(executable);
×
178
      int exitCode = pc.run(ProcessMode.BACKGROUND_SILENT).getExitCode();
×
179
      if (tmpDir != null) {
×
180
        fileAccess.delete(tmpDir);
×
181
      }
182
      if (exitCode != 0) {
×
183
        throw new CliException("Installation process for " + this.tool + " in version " + resolvedVersion + " failed with exit code " + exitCode + "!");
×
184
      }
185
    }
186
    IdeLogLevel.SUCCESS.log(LOG, "Installation process for {} in version {} has started", this.tool, resolvedVersion);
×
187
    Step step = request.getStep();
×
188
    if (step != null) {
×
189
      step.success(true);
×
190
    }
191
    installationPath = getInstallationPath(toolEdition.edition(), resolvedVersion);
×
192
    if (installationPath == null) {
×
193
      throw new CliException("The tool " + this.tool + " is about to be installed. Please complete the installation and if required "
×
194
          + "reboot your machine. Then rerun the command to start the tool.", 2);
195
    }
196
    return createToolInstallation(installationPath, resolvedVersion, true, pc, false);
×
197
  }
198

199
  private void installMacDmg(Path downloadedToolFile) {
200

201
    FileAccess fileAccess = this.context.getFileAccess();
×
202
    Path tmpDir = fileAccess.createTempDir(getName());
×
203
    try {
204
      fileAccess.extractDmg(downloadedToolFile, tmpDir);
×
205
      Path sourceApp = getMacOsHelper().findAppDir(tmpDir);
×
206
      if (sourceApp == null) {
×
207
        throw new CliException("Failed to install " + this.tool + " from " + downloadedToolFile + " because no MacOS *.app was found.");
×
208
      }
209
      Path targetApp = getMacApplicationsPath().resolve(sourceApp.getFileName().toString());
×
210
      copyMacApplicationToApplications(sourceApp, targetApp);
×
211
    } finally {
212
      fileAccess.delete(tmpDir);
×
213
    }
214
  }
×
215

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

224
    runPrivilegedCommands(List.of(
×
225
        List.of("/bin/rm", "-rf", targetApp.toString()),
×
226
        List.of("/usr/bin/ditto", sourceApp.toString(), targetApp.toString())));
×
227
  }
×
228

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

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

240
  private String toSudoCommandLine(List<String> command) {
241

242
    return "sudo " + String.join(" ", command);
×
243
  }
244

245
  private boolean isMacDmg(Path file) {
246

247
    if (!this.context.getSystemInfo().isMac()) {
×
248
      return false;
×
249
    }
250
    String extension = FilenameUtil.getExtension(file.toString());
×
251
    return "dmg".equals(extension);
×
252
  }
253

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

259
    return Path.of("/Applications");
×
260
  }
261

262
  /**
263
   * @return the {@link List} of {@link PackageManagerCommand}s to use on Linux to install this tool. If empty, no package manager installation will be
264
   *     triggered on Linux.
265
   */
266
  protected List<PackageManagerCommand> getInstallPackageManagerCommands() {
267
    return List.of();
×
268
  }
269

270
  @Override
271
  public VersionIdentifier getInstalledVersion() {
272
    //TODO: handle "get-version <globaltool>"
273
    return null;
×
274
  }
275

276
  @Override
277
  public String getInstalledEdition() {
278
    //TODO: handle "get-edition <globaltool>"
279
    return null;
×
280
  }
281

282
  @Override
283
  protected Path getInstallationPath(String edition, VersionIdentifier resolvedVersion) {
284

285
    Path toolBinary = Path.of(getBinaryName());
6✔
286
    Path binaryPath = this.context.getPath().findBinary(toolBinary);
6✔
287
    if ((binaryPath == toolBinary) || !Files.exists(binaryPath)) {
8!
288
      if (this.context.getSystemInfo().isMac()) {
5✔
289
        return getMacApplicationInstallationPath();
3✔
290
      }
291
      return null;
2✔
292
    }
293
    Path binPath = binaryPath.getParent();
3✔
294
    if (binPath == null) {
2!
295
      return null;
×
296
    }
297
    return this.context.getFileAccess().getBinParentPath(binPath);
6✔
298
  }
299

300
  private Path getMacApplicationInstallationPath() {
301

302
    Path appPath = this.context.getFileAccess().findFirst(getMacApplicationsPath(), this::isMacApplicationForTool, false);
10✔
303
    if (appPath == null) {
2!
304
      return null;
×
305
    }
306
    Path binaryPath = getMacApplicationBinaryPath(appPath);
4✔
307
    this.context.getPath().setPath(getName(), binaryPath.getParent());
8✔
308
    return appPath;
2✔
309
  }
310

311
  private boolean isMacApplicationForTool(Path appPath) {
312

313
    if (!Files.isDirectory(appPath) || !appPath.getFileName().toString().endsWith(".app")) {
11!
314
      return false;
×
315
    }
316
    return Files.isExecutable(getMacApplicationBinaryPath(appPath));
5✔
317
  }
318

319
  private Path getMacApplicationBinaryPath(Path appPath) {
320

321
    return appPath.resolve(IdeContext.FOLDER_CONTENTS).resolve(IdeContext.FOLDER_MAC_OS).resolve(getBinaryName());
9✔
322
  }
323

324
  @Override
325
  public void uninstall() {
326
    //TODO: handle "uninstall <globaltool>"
327
    LOG.error("Couldn't uninstall " + this.getName());
×
328
  }
×
329
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc