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

devonfw / IDEasy / 19789527581

29 Nov 2025 09:25PM UTC coverage: 69.431% (-0.06%) from 69.489%
19789527581

push

github

web-flow
#1615: prevent to ask CVE questions if tool already installed (#1618)

3696 of 5853 branches covered (63.15%)

Branch coverage included in aggregate %.

9618 of 13323 relevant lines covered (72.19%)

3.14 hits per line

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

76.91
cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java
1
package com.devonfw.tools.ide.tool;
2

3
import java.io.IOException;
4
import java.nio.file.Files;
5
import java.nio.file.Path;
6
import java.util.ArrayList;
7
import java.util.Collection;
8
import java.util.List;
9
import java.util.Set;
10

11
import com.devonfw.tools.ide.commandlet.Commandlet;
12
import com.devonfw.tools.ide.common.Tag;
13
import com.devonfw.tools.ide.common.Tags;
14
import com.devonfw.tools.ide.context.IdeContext;
15
import com.devonfw.tools.ide.environment.EnvironmentVariables;
16
import com.devonfw.tools.ide.environment.EnvironmentVariablesFiles;
17
import com.devonfw.tools.ide.io.FileCopyMode;
18
import com.devonfw.tools.ide.log.IdeSubLogger;
19
import com.devonfw.tools.ide.nls.NlsBundle;
20
import com.devonfw.tools.ide.os.MacOsHelper;
21
import com.devonfw.tools.ide.process.EnvironmentContext;
22
import com.devonfw.tools.ide.process.ProcessContext;
23
import com.devonfw.tools.ide.process.ProcessErrorHandling;
24
import com.devonfw.tools.ide.process.ProcessMode;
25
import com.devonfw.tools.ide.process.ProcessResult;
26
import com.devonfw.tools.ide.property.StringProperty;
27
import com.devonfw.tools.ide.security.ToolVersionChoice;
28
import com.devonfw.tools.ide.step.Step;
29
import com.devonfw.tools.ide.tool.repository.ToolRepository;
30
import com.devonfw.tools.ide.url.model.file.json.Cve;
31
import com.devonfw.tools.ide.url.model.file.json.ToolDependency;
32
import com.devonfw.tools.ide.url.model.file.json.ToolSecurity;
33
import com.devonfw.tools.ide.variable.IdeVariables;
34
import com.devonfw.tools.ide.version.GenericVersionRange;
35
import com.devonfw.tools.ide.version.VersionIdentifier;
36

37
/**
38
 * {@link Commandlet} for a tool integrated into the IDE.
39
 */
40
public abstract class ToolCommandlet extends Commandlet implements Tags {
1✔
41

42
  /** @see #getName() */
43
  protected final String tool;
44

45
  private final Set<Tag> tags;
46

47
  /** The commandline arguments to pass to the tool. */
48
  public final StringProperty arguments;
49

50
  private Path executionDirectory;
51

52
  private MacOsHelper macOsHelper;
53

54
  /**
55
   * The constructor.
56
   *
57
   * @param context the {@link IdeContext}.
58
   * @param tool the {@link #getName() tool name}.
59
   * @param tags the {@link #getTags() tags} classifying the tool. Should be created via {@link Set#of(Object) Set.of} method.
60
   */
61
  public ToolCommandlet(IdeContext context, String tool, Set<Tag> tags) {
62

63
    super(context);
3✔
64
    this.tool = tool;
3✔
65
    this.tags = tags;
3✔
66
    addKeyword(tool);
3✔
67
    this.arguments = new StringProperty("", false, true, "args");
9✔
68
    initProperties();
2✔
69
  }
1✔
70

71
  /**
72
   * Add initial Properties to the tool
73
   */
74
  protected void initProperties() {
75

76
    add(this.arguments);
5✔
77
  }
1✔
78

79
  /**
80
   * @return the name of the tool (e.g. "java", "mvn", "npm", "node").
81
   */
82
  @Override
83
  public final String getName() {
84

85
    return this.tool;
3✔
86
  }
87

88
  /**
89
   * @return the name of the binary executable for this tool.
90
   */
91
  protected String getBinaryName() {
92

93
    return this.tool;
3✔
94
  }
95

96
  @Override
97
  public final Set<Tag> getTags() {
98

99
    return this.tags;
3✔
100
  }
101

102
  /**
103
   * @return the execution directory where the tool will be executed. Will be {@code null} by default leading to execution in the users current working
104
   *     directory where IDEasy was called.
105
   * @see #setExecutionDirectory(Path)
106
   */
107
  public Path getExecutionDirectory() {
108
    return this.executionDirectory;
×
109
  }
110

111
  /**
112
   * @param executionDirectory the new value of {@link #getExecutionDirectory()}.
113
   */
114
  public void setExecutionDirectory(Path executionDirectory) {
115
    this.executionDirectory = executionDirectory;
×
116
  }
×
117

118
  /**
119
   * @return the {@link EnvironmentVariables#getToolVersion(String) tool version}.
120
   */
121
  public VersionIdentifier getConfiguredVersion() {
122

123
    return this.context.getVariables().getToolVersion(getName());
7✔
124
  }
125

126
  /**
127
   * @return the {@link EnvironmentVariables#getToolEdition(String) tool edition}.
128
   */
129
  public String getConfiguredEdition() {
130

131
    return this.context.getVariables().getToolEdition(getName());
7✔
132
  }
133

134
  /**
135
   * @return the {@link ToolEdition} with {@link #getName() tool} with its {@link #getConfiguredEdition() edition}.
136
   */
137
  protected final ToolEdition getToolWithConfiguredEdition() {
138

139
    return new ToolEdition(this.tool, getConfiguredEdition());
8✔
140
  }
141

142
  @Override
143
  public void run() {
144

145
    runTool(this.arguments.asArray());
6✔
146
  }
1✔
147

148
  /**
149
   * @param args the command-line arguments to run the tool.
150
   * @return the {@link ProcessResult result}.
151
   * @see ToolCommandlet#runTool(ProcessMode, GenericVersionRange, String...)
152
   */
153
  public ProcessResult runTool(String... args) {
154

155
    return runTool(ProcessMode.DEFAULT, null, args);
6✔
156
  }
157

158
  /**
159
   * Ensures the tool is installed and then runs this tool with the given arguments.
160
   *
161
   * @param processMode the {@link ProcessMode}. Should typically be {@link ProcessMode#DEFAULT} or {@link ProcessMode#BACKGROUND}.
162
   * @param toolVersion the explicit {@link GenericVersionRange version} to run. Typically {@code null} to run the
163
   *     {@link #getConfiguredVersion() configured version}. Otherwise, the specified version will be used (from the software repository, if not compatible).
164
   * @param args the command-line arguments to run the tool.
165
   * @return the {@link ProcessResult result}.
166
   */
167
  public final ProcessResult runTool(ProcessMode processMode, GenericVersionRange toolVersion, String... args) {
168

169
    return runTool(processMode, toolVersion, ProcessErrorHandling.THROW_CLI, args);
7✔
170
  }
171

172
  /**
173
   * Ensures the tool is installed and then runs this tool with the given arguments.
174
   *
175
   * @param processMode the {@link ProcessMode}. Should typically be {@link ProcessMode#DEFAULT} or {@link ProcessMode#BACKGROUND}.
176
   * @param toolVersion the explicit {@link GenericVersionRange version} to run. Typically {@code null} to run the
177
   *     {@link #getConfiguredVersion() configured version}. Otherwise, the specified version will be used (from the software repository, if not compatible).
178
   * @param errorHandling the {@link ProcessErrorHandling}.
179
   * @param args the command-line arguments to run the tool.
180
   * @return the {@link ProcessResult result}.
181
   */
182
  public ProcessResult runTool(ProcessMode processMode, GenericVersionRange toolVersion, ProcessErrorHandling errorHandling, String... args) {
183

184
    ProcessContext pc = this.context.newProcess().errorHandling(errorHandling);
6✔
185
    ToolInstallRequest request = new ToolInstallRequest(true);
5✔
186
    if (toolVersion != null) {
2!
187
      request.setRequested(new ToolEditionAndVersion(toolVersion));
×
188
    }
189
    request.setProcessContext(pc);
3✔
190
    install(request);
4✔
191
    return runTool(pc, processMode, args);
6✔
192
  }
193

194
  /**
195
   * @param pc the {@link ProcessContext}.
196
   * @param processMode the {@link ProcessMode}. Should typically be {@link ProcessMode#DEFAULT} or {@link ProcessMode#BACKGROUND}.
197
   * @param args the command-line arguments to run the tool.
198
   * @return the {@link ProcessResult result}.
199
   */
200
  public ProcessResult runTool(ProcessContext pc, ProcessMode processMode, String... args) {
201

202
    if (this.executionDirectory != null) {
3!
203
      pc.directory(this.executionDirectory);
×
204
    }
205
    configureToolBinary(pc, processMode);
4✔
206
    configureToolArgs(pc, processMode, args);
5✔
207
    return pc.run(processMode);
4✔
208
  }
209

210
  /**
211
   * @param pc the {@link ProcessContext}.
212
   * @param processMode the {@link ProcessMode}.
213
   */
214
  protected void configureToolBinary(ProcessContext pc, ProcessMode processMode) {
215

216
    pc.executable(Path.of(getBinaryName()));
8✔
217
  }
1✔
218

219
  /**
220
   * @param pc the {@link ProcessContext}.
221
   * @param processMode the {@link ProcessMode}.
222
   * @param args the command-line arguments to {@link ProcessContext#addArgs(Object...) add}.
223
   */
224
  protected void configureToolArgs(ProcessContext pc, ProcessMode processMode, String... args) {
225

226
    pc.addArgs(args);
4✔
227
  }
1✔
228

229
  /**
230
   * Installs or updates the managed {@link #getName() tool}.
231
   *
232
   * @return the {@link ToolInstallation}.
233
   */
234
  public ToolInstallation install() {
235

236
    return install(true);
4✔
237
  }
238

239
  /**
240
   * Performs the installation of the {@link #getName() tool} managed by this {@link com.devonfw.tools.ide.commandlet.Commandlet}.
241
   *
242
   * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
243
   * @return the {@link ToolInstallation}.
244
   */
245
  public ToolInstallation install(boolean silent) {
246
    return install(new ToolInstallRequest(silent));
7✔
247
  }
248

249
  /**
250
   * Performs the installation (install, update, downgrade) of the {@link #getName() tool} managed by this {@link ToolCommandlet}.
251
   *
252
   * @param request the {@link ToolInstallRequest}.
253
   * @return the {@link ToolInstallation}.
254
   */
255
  public ToolInstallation install(ToolInstallRequest request) {
256

257
    completeRequest(request);
3✔
258
    return doInstall(request);
4✔
259
  }
260

261
  /**
262
   * Performs the installation (install, update, downgrade) of the {@link #getName() tool} managed by this {@link ToolCommandlet}.
263
   *
264
   * @param request the {@link ToolInstallRequest}.
265
   * @return the {@link ToolInstallation}.
266
   */
267
  protected abstract ToolInstallation doInstall(ToolInstallRequest request);
268

269
  /**
270
   * @param request the {@link ToolInstallRequest} to complete (fill values that are currently {@code null}).
271
   */
272
  protected void completeRequest(ToolInstallRequest request) {
273

274
    completeRequestInstalled(request);
3✔
275
    completeRequestRequested(request); // depends on completeRequestInstalled
3✔
276
    completeRequestProcessContext(request);
3✔
277
  }
1✔
278

279
  private void completeRequestProcessContext(ToolInstallRequest request) {
280
    if (request.getProcessContext() == null) {
3✔
281
      ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_CLI);
6✔
282
      request.setProcessContext(pc);
3✔
283
    }
284
  }
1✔
285

286
  private void completeRequestInstalled(ToolInstallRequest request) {
287

288
    ToolEditionAndVersion installedToolVersion = request.getInstalled();
3✔
289
    if (installedToolVersion == null) {
2✔
290
      installedToolVersion = new ToolEditionAndVersion((GenericVersionRange) null);
6✔
291
      request.setInstalled(installedToolVersion);
3✔
292
    }
293
    if (installedToolVersion.getVersion() == null) {
3✔
294
      VersionIdentifier installedVersion = getInstalledVersion();
3✔
295
      if (installedVersion == null) {
2✔
296
        return;
1✔
297
      }
298
      installedToolVersion.setVersion(installedVersion);
3✔
299
    }
300
    if (installedToolVersion.getEdition() == null) {
3✔
301
      installedToolVersion.setEdition(new ToolEdition(this.tool, getInstalledEdition()));
9✔
302
    }
303
    assert installedToolVersion.getResolvedVersion() != null;
4!
304
  }
1✔
305

306
  private void completeRequestRequested(ToolInstallRequest request) {
307

308
    ToolEdition edition;
309
    ToolEditionAndVersion requested = request.getRequested();
3✔
310
    if (requested == null) {
2✔
311
      edition = new ToolEdition(this.tool, getConfiguredEdition());
8✔
312
      requested = new ToolEditionAndVersion(edition);
5✔
313
      request.setRequested(requested);
4✔
314
    } else {
315
      edition = requested.getEdition();
3✔
316
      if (edition == null) {
2✔
317
        edition = new ToolEdition(this.tool, getConfiguredEdition());
8✔
318
        requested.setEdition(edition);
3✔
319
      }
320
    }
321
    GenericVersionRange version = requested.getVersion();
3✔
322
    if (version == null) {
2✔
323
      version = getConfiguredVersion();
3✔
324
      requested.setVersion(version);
3✔
325
    }
326
    VersionIdentifier resolvedVersion = requested.getResolvedVersion();
3✔
327
    if (resolvedVersion == null) {
2✔
328
      if (this.context.isSkipUpdatesMode()) {
4✔
329
        ToolEditionAndVersion installed = request.getInstalled();
3✔
330
        if (installed != null) {
2!
331
          VersionIdentifier installedVersion = installed.getResolvedVersion();
3✔
332
          if (version.contains(installedVersion)) {
4✔
333
            resolvedVersion = installedVersion;
2✔
334
          }
335
        }
336
      }
337
      if (resolvedVersion == null) {
2✔
338
        resolvedVersion = getToolRepository().resolveVersion(this.tool, edition.edition(), version, this);
10✔
339
      }
340
      requested.setResolvedVersion(resolvedVersion);
3✔
341
    }
342
  }
1✔
343

344
  /**
345
   * This method is called after a tool was requested to be installed or updated.
346
   *
347
   * @param newlyInstalled {@code true} if the tool was installed or updated (at least link to software folder was created/updated), {@code false} otherwise
348
   *     (configured version was already installed and nothing changed).
349
   * @param pc the {@link ProcessContext} to use.
350
   */
351
  protected void postInstall(boolean newlyInstalled, ProcessContext pc) {
352

353
    if (newlyInstalled) {
2✔
354
      postInstall();
2✔
355
    }
356
  }
1✔
357

358
  /**
359
   * This method is called after the tool has been newly installed or updated to a new version.
360
   */
361
  protected void postInstall() {
362

363
    // nothing to do by default
364
  }
1✔
365

366
  /**
367
   * @param edition the {@link #getInstalledEdition() edition}.
368
   * @param version the {@link #getInstalledVersion() version}.
369
   * @return the {@link Path} where this tool is installed (physically) or {@code null} if not available.
370
   */
371
  protected abstract Path getInstallationPath(String edition, VersionIdentifier version);
372

373
  /**
374
   * @param request the {@link ToolInstallRequest}.
375
   * @return the existing {@link ToolInstallation}.
376
   */
377
  protected ToolInstallation createExistingToolInstallation(ToolInstallRequest request) {
378

379
    ToolEditionAndVersion installed = request.getInstalled();
3✔
380
    return createExistingToolInstallation(installed.getEdition().edition(), installed.getResolvedVersion(), request.getProcessContext(),
11✔
381
        request.isAdditionalInstallation());
1✔
382
  }
383

384
  /**
385
   * @param edition the {@link #getConfiguredEdition() edition}.
386
   * @param installedVersion the {@link #getConfiguredVersion() version}.
387
   * @param environmentContext the {@link EnvironmentContext}.
388
   * @param extraInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
389
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
390
   * @return the {@link ToolInstallation}.
391
   */
392
  protected ToolInstallation createExistingToolInstallation(String edition, VersionIdentifier installedVersion, EnvironmentContext environmentContext,
393
      boolean extraInstallation) {
394

395
    Path installationPath = getInstallationPath(edition, installedVersion);
5✔
396
    return createToolInstallation(installationPath, installedVersion, false, environmentContext, extraInstallation);
8✔
397
  }
398

399
  /**
400
   * @param rootDir the {@link ToolInstallation#rootDir() top-level installation directory}.
401
   * @param version the installed {@link VersionIdentifier}.
402
   * @param newInstallation {@link ToolInstallation#newInstallation() new installation} flag.
403
   * @param environmentContext the {@link EnvironmentContext}.
404
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
405
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
406
   * @return the {@link ToolInstallation}.
407
   */
408
  protected ToolInstallation createToolInstallation(Path rootDir, VersionIdentifier version, boolean newInstallation,
409
      EnvironmentContext environmentContext, boolean additionalInstallation) {
410

411
    Path linkDir = rootDir;
2✔
412
    Path binDir = rootDir;
2✔
413
    if (rootDir != null) {
2✔
414
      // on MacOS applications have a very strange structure - see JavaDoc of findLinkDir and ToolInstallation.linkDir for details.
415
      linkDir = getMacOsHelper().findLinkDir(rootDir, getBinaryName());
7✔
416
      binDir = this.context.getFileAccess().getBinPath(linkDir);
6✔
417
    }
418
    return createToolInstallation(rootDir, linkDir, binDir, version, newInstallation, environmentContext, additionalInstallation);
10✔
419
  }
420

421
  /**
422
   * @param rootDir the {@link ToolInstallation#rootDir() top-level installation directory}.
423
   * @param linkDir the {@link ToolInstallation#linkDir() link directory}.
424
   * @param binDir the {@link ToolInstallation#binDir() bin directory}.
425
   * @param version the installed {@link VersionIdentifier}.
426
   * @param newInstallation {@link ToolInstallation#newInstallation() new installation} flag.
427
   * @param environmentContext the {@link EnvironmentContext}.
428
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
429
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
430
   * @return the {@link ToolInstallation}.
431
   */
432
  protected ToolInstallation createToolInstallation(Path rootDir, Path linkDir, Path binDir, VersionIdentifier version, boolean newInstallation,
433
      EnvironmentContext environmentContext, boolean additionalInstallation) {
434

435
    if (linkDir != rootDir) {
3✔
436
      assert (!linkDir.equals(rootDir));
5!
437
      Path toolVersionFile = rootDir.resolve(IdeContext.FILE_SOFTWARE_VERSION);
4✔
438
      if (Files.exists(toolVersionFile)) {
5!
439
        this.context.getFileAccess().copy(toolVersionFile, linkDir, FileCopyMode.COPY_FILE_OVERRIDE);
7✔
440
      }
441
    }
442
    ToolInstallation toolInstallation = new ToolInstallation(rootDir, linkDir, binDir, version, newInstallation);
9✔
443
    setEnvironment(environmentContext, toolInstallation, additionalInstallation);
5✔
444
    return toolInstallation;
2✔
445
  }
446

447
  /**
448
   * Called if the tool {@link ToolInstallRequest#isAlreadyInstalled(boolean) is already installed in the correct edition and version} so we can skip the
449
   * installation.
450
   *
451
   * @param request the {@link ToolInstallRequest}.
452
   * @return the {@link ToolInstallation}.
453
   */
454
  protected ToolInstallation toolAlreadyInstalled(ToolInstallRequest request) {
455

456
    logToolAlreadyInstalled(request);
3✔
457
    cveCheck(request);
4✔
458
    postInstall(false, request.getProcessContext());
5✔
459
    return createExistingToolInstallation(request);
4✔
460
  }
461

462
  /**
463
   * Log that the tool is already installed.
464
   *
465
   * @param request the {@link ToolInstallRequest}.
466
   */
467
  protected void logToolAlreadyInstalled(ToolInstallRequest request) {
468
    IdeSubLogger logger;
469
    if (request.isSilent()) {
3✔
470
      logger = this.context.debug();
5✔
471
    } else {
472
      logger = this.context.info();
4✔
473
    }
474
    ToolEditionAndVersion installed = request.getInstalled();
3✔
475
    logger.log("Version {} of tool {} is already installed", installed.getVersion(), installed.getEdition());
16✔
476
  }
1✔
477

478
  /**
479
   * Method to get the home path of the given {@link ToolInstallation}.
480
   *
481
   * @param toolInstallation the {@link ToolInstallation}.
482
   * @return the Path to the home of the tool
483
   */
484
  protected Path getToolHomePath(ToolInstallation toolInstallation) {
485
    return toolInstallation.linkDir();
3✔
486
  }
487

488
  /**
489
   * Method to set environment variables for the process context.
490
   *
491
   * @param environmentContext the {@link EnvironmentContext} where to {@link EnvironmentContext#withEnvVar(String, String) set environment variables} for
492
   *     this tool.
493
   * @param toolInstallation the {@link ToolInstallation}.
494
   * @param extraInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
495
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
496
   */
497
  public void setEnvironment(EnvironmentContext environmentContext, ToolInstallation toolInstallation, boolean extraInstallation) {
498

499
    String pathVariable = EnvironmentVariables.getToolVariablePrefix(this.tool) + "_HOME";
5✔
500
    Path toolHomePath = getToolHomePath(toolInstallation);
4✔
501
    if (toolHomePath != null) {
2✔
502
      environmentContext.withEnvVar(pathVariable, toolHomePath.toString());
6✔
503
    }
504
    if (extraInstallation) {
2✔
505
      environmentContext.withPathEntry(toolInstallation.binDir());
5✔
506
    }
507
  }
1✔
508

509
  /**
510
   * @return {@code true} to extract (unpack) the downloaded binary file, {@code false} otherwise.
511
   */
512
  protected boolean isExtract() {
513

514
    return true;
2✔
515
  }
516

517
  /**
518
   * Checks a version to be installed for {@link Cve}s. If at least one {@link Cve} is found, we try to find better/safer versions as alternative. If we find
519
   * something better, we will suggest this to the user and ask him to make his choice.
520
   *
521
   * @param request the {@link ToolInstallRequest}.
522
   * @return the {@link VersionIdentifier} to install. The will may be asked (unless {@code skipSuggestions} is {@code true}) and might choose a different
523
   *     version than the originally requested one.
524
   */
525
  protected VersionIdentifier cveCheck(ToolInstallRequest request) {
526

527
    ToolEditionAndVersion requested = request.getRequested();
3✔
528
    VersionIdentifier resolvedVersion = requested.getResolvedVersion();
3✔
529
    if (request.isCveCheckDone()) {
3✔
530
      return resolvedVersion;
2✔
531
    }
532
    ToolEdition toolEdition = requested.getEdition();
3✔
533
    GenericVersionRange allowedVersions = requested.getVersion();
3✔
534
    ToolSecurity toolSecurity = this.context.getDefaultToolRepository().findSecurity(this.tool, toolEdition.edition());
9✔
535
    double minSeverity = IdeVariables.CVE_MIN_SEVERITY.get(context);
7✔
536
    Collection<Cve> issues = toolSecurity.findCves(resolvedVersion, this.context, minSeverity);
7✔
537
    ToolVersionChoice currentChoice = ToolVersionChoice.ofCurrent(resolvedVersion, issues);
4✔
538
    request.setCveCheckDone();
2✔
539
    if (logCvesAndReturnTrueForNone(toolEdition, resolvedVersion, currentChoice.option(), issues)) {
8✔
540
      return resolvedVersion;
2✔
541
    }
542
    boolean alreadyInstalled = request.isAlreadyInstalled();
3✔
543
    boolean directForceInstall = this.context.isForceMode() && request.isDirect();
6!
544
    if (alreadyInstalled && !directForceInstall) {
2!
545
      // currently for a transitive dependency it does not make sense to suggest alternative versions, since the choice is not stored anywhere,
546
      // and we then would ask the user again every time the tool having this dependency is started. So we only log the problem and the user needs to react
547
      // (e.g. upgrade the tool with the dependency that is causing this).
548
      this.context.interaction("Please run 'ide -f install {}' to check for update suggestions!", this.tool);
×
549
      return resolvedVersion;
×
550
    }
551
    double currentSeveritySum = Cve.severitySum(issues);
3✔
552
    ToolVersionChoice latest = null;
2✔
553
    ToolVersionChoice nearest = null;
2✔
554
    List<VersionIdentifier> toolVersions = getVersions();
3✔
555
    double latestSeveritySum = currentSeveritySum;
2✔
556
    double nearestSeveritySum = currentSeveritySum;
2✔
557
    for (VersionIdentifier version : toolVersions) {
10✔
558
      if (!allowedVersions.isPattern() || allowedVersions.contains(version)) {
3!
559
        issues = toolSecurity.findCves(version, this.context, minSeverity);
7✔
560
        double newSeveritySum = Cve.severitySum(issues);
3✔
561
        if (newSeveritySum < latestSeveritySum) {
4✔
562
          // we found a better/safer version
563
          if (version.isGreater(resolvedVersion)) {
4!
564
            latest = ToolVersionChoice.ofLatest(version, issues);
4✔
565
            nearest = null;
2✔
566
            latestSeveritySum = newSeveritySum;
3✔
567
          } else {
568
            // latest = null;
569
            nearest = ToolVersionChoice.ofNearest(version, issues);
×
570
            nearestSeveritySum = newSeveritySum;
×
571
          }
572
        } else if (newSeveritySum < nearestSeveritySum) {
4✔
573
          if (version.isGreater(resolvedVersion)) {
4!
574
            nearest = ToolVersionChoice.ofNearest(version, issues);
×
575
          } else if (nearest == null) {
2!
576
            nearest = ToolVersionChoice.ofNearest(version, issues);
4✔
577
          }
578
          nearestSeveritySum = newSeveritySum;
2✔
579
        }
580
      }
581
    }
1✔
582
    if ((latest == null) && (nearest == null)) {
2!
583
      this.context.warning(
×
584
          "Could not find any other version resolving your CVEs.\nPlease keep attention to this tool and consider updating as soon as security fixes are available.");
585
      if (alreadyInstalled) {
×
586
        // we came here via "ide -f install ..." but no alternative is available
587
        return resolvedVersion;
×
588
      }
589
    }
590
    List<ToolVersionChoice> choices = new ArrayList<>();
4✔
591
    choices.add(currentChoice);
4✔
592
    boolean addSuggestions;
593
    if (this.context.isForceMode() && request.isDirect()) {
4!
594
      addSuggestions = true;
×
595
    } else {
596
      List<String> skipCveFixTools = IdeVariables.SKIP_CVE_FIX.get(this.context);
6✔
597
      addSuggestions = !skipCveFixTools.contains(this.tool);
8!
598
    }
599
    if (nearest != null) {
2!
600
      if (addSuggestions) {
2!
601
        choices.add(nearest);
4✔
602
      }
603
      logCvesAndReturnTrueForNone(toolEdition, nearest.version(), nearest.option(), nearest.issues());
10✔
604
    }
605
    if (latest != null) {
2!
606
      if (addSuggestions) {
2!
607
        choices.add(latest);
4✔
608
      }
609
      logCvesAndReturnTrueForNone(toolEdition, latest.version(), latest.option(), latest.issues());
10✔
610
    }
611
    ToolVersionChoice[] choicesArray = choices.toArray(ToolVersionChoice[]::new);
8✔
612
    this.context.warning(
4✔
613
        "Please note that by selecting an unsafe version to install, you accept the risk to be attacked.");
614
    ToolVersionChoice answer = this.context.question(choicesArray, "Which version do you want to install?");
9✔
615
    VersionIdentifier version = answer.version();
3✔
616
    requested.setResolvedVersion(version);
3✔
617
    return version;
2✔
618
  }
619

620
  private boolean logCvesAndReturnTrueForNone(ToolEdition toolEdition, VersionIdentifier version, String option, Collection<Cve> issues) {
621
    if (issues.isEmpty()) {
3✔
622
      this.context.info("No CVEs found for {} version {} of tool {}.", option, version, toolEdition);
18✔
623
      return true;
2✔
624
    }
625
    this.context.warning("For {} version {} of tool {} we found {} CVE(s):", option, version, toolEdition, issues.size());
24✔
626
    for (Cve cve : issues) {
10✔
627
      logCve(cve);
3✔
628
    }
1✔
629
    return false;
2✔
630
  }
631

632
  private void logCve(Cve cve) {
633

634
    this.context.warning("{} with severity {} and affected versions: {} ", cve.id(), cve.severity(), cve.versions());
22✔
635
    this.context.warning("https://nvd.nist.gov/vuln/detail/" + cve.id());
6✔
636
    this.context.info("");
4✔
637
  }
1✔
638

639
  /**
640
   * @return the {@link MacOsHelper} instance.
641
   */
642
  protected MacOsHelper getMacOsHelper() {
643

644
    if (this.macOsHelper == null) {
3✔
645
      this.macOsHelper = new MacOsHelper(this.context);
7✔
646
    }
647
    return this.macOsHelper;
3✔
648
  }
649

650
  /**
651
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
652
   */
653
  public abstract VersionIdentifier getInstalledVersion();
654

655
  /**
656
   * @return {@code true} if this tool is installed, {@code false} otherwise.
657
   */
658
  public boolean isInstalled() {
659

660
    return getInstalledVersion() != null;
7✔
661
  }
662

663
  /**
664
   * @return the installed edition of this tool or {@code null} if not installed.
665
   */
666
  public abstract String getInstalledEdition();
667

668
  /**
669
   * Uninstalls the {@link #getName() tool}.
670
   */
671
  public abstract void uninstall();
672

673
  /**
674
   * @return the {@link ToolRepository}.
675
   */
676
  public ToolRepository getToolRepository() {
677

678
    return this.context.getDefaultToolRepository();
4✔
679
  }
680

681
  /**
682
   * List the available editions of this tool.
683
   */
684
  public void listEditions() {
685

686
    List<String> editions = getToolRepository().getSortedEditions(getName());
6✔
687
    for (String edition : editions) {
10✔
688
      this.context.info(edition);
4✔
689
    }
1✔
690
  }
1✔
691

692
  /**
693
   * List the available versions of this tool.
694
   */
695
  public void listVersions() {
696

697
    List<VersionIdentifier> versions = getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
698
    for (VersionIdentifier vi : versions) {
10✔
699
      this.context.info(vi.toString());
5✔
700
    }
1✔
701
  }
1✔
702

703
  /**
704
   * @return the {@link com.devonfw.tools.ide.tool.repository.DefaultToolRepository#getSortedVersions(String, String, ToolCommandlet) sorted versions} of this
705
   *     tool.
706
   */
707
  public List<VersionIdentifier> getVersions() {
708
    return getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
709
  }
710

711
  /**
712
   * Sets the tool version in the environment variable configuration file.
713
   *
714
   * @param version the version (pattern) to set.
715
   */
716
  public void setVersion(String version) {
717

718
    if ((version == null) || version.isBlank()) {
×
719
      throw new IllegalStateException("Version has to be specified!");
×
720
    }
721
    VersionIdentifier configuredVersion = VersionIdentifier.of(version);
×
722
    if (!configuredVersion.isPattern() && !configuredVersion.isValid()) {
×
723
      this.context.warning("Version {} seems to be invalid", version);
×
724
    }
725
    setVersion(configuredVersion, true);
×
726
  }
×
727

728
  /**
729
   * Sets the tool version in the environment variable configuration file.
730
   *
731
   * @param version the version to set. May also be a {@link VersionIdentifier#isPattern() version pattern}.
732
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
733
   */
734
  public void setVersion(VersionIdentifier version, boolean hint) {
735

736
    setVersion(version, hint, null);
5✔
737
  }
1✔
738

739
  /**
740
   * Sets the tool version in the environment variable configuration file.
741
   *
742
   * @param version the version to set. May also be a {@link VersionIdentifier#isPattern() version pattern}.
743
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
744
   * @param destination - the destination for the property to be set
745
   */
746
  public void setVersion(VersionIdentifier version, boolean hint, EnvironmentVariablesFiles destination) {
747

748
    String edition = getConfiguredEdition();
3✔
749
    ToolRepository toolRepository = getToolRepository();
3✔
750

751
    EnvironmentVariables variables = this.context.getVariables();
4✔
752
    if (destination == null) {
2✔
753
      //use default location
754
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
755
    }
756
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
757
    String name = EnvironmentVariables.getToolVersionVariable(this.tool);
4✔
758

759
    toolRepository.resolveVersion(this.tool, edition, version, this); // verify that the version actually exists
8✔
760
    settingsVariables.set(name, version.toString(), false);
7✔
761
    settingsVariables.save();
2✔
762
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
763
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
764
      this.context.warning("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name,
13✔
765
          declaringVariables.getSource());
2✔
766
    }
767
    if (hint) {
2✔
768
      this.context.info("To install that version call the following command:");
4✔
769
      this.context.info("ide install {}", this.tool);
11✔
770
    }
771
  }
1✔
772

773
  /**
774
   * Sets the tool edition in the environment variable configuration file.
775
   *
776
   * @param edition the edition to set.
777
   */
778
  public void setEdition(String edition) {
779

780
    setEdition(edition, true);
4✔
781
  }
1✔
782

783
  /**
784
   * Sets the tool edition in the environment variable configuration file.
785
   *
786
   * @param edition the edition to set
787
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
788
   */
789
  public void setEdition(String edition, boolean hint) {
790

791
    setEdition(edition, hint, null);
5✔
792
  }
1✔
793

794
  /**
795
   * Sets the tool edition in the environment variable configuration file.
796
   *
797
   * @param edition the edition to set
798
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
799
   * @param destination - the destination for the property to be set
800
   */
801
  public void setEdition(String edition, boolean hint, EnvironmentVariablesFiles destination) {
802

803
    if ((edition == null) || edition.isBlank()) {
5!
804
      throw new IllegalStateException("Edition has to be specified!");
×
805
    }
806

807
    if (destination == null) {
2✔
808
      //use default location
809
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
810
    }
811

812
    if (!getToolRepository().getSortedEditions(this.tool).contains(edition)) {
8✔
813
      this.context.warning("Edition {} seems to be invalid", edition);
10✔
814
    }
815
    EnvironmentVariables variables = this.context.getVariables();
4✔
816
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
817
    String name = EnvironmentVariables.getToolEditionVariable(this.tool);
4✔
818
    settingsVariables.set(name, edition, false);
6✔
819
    settingsVariables.save();
2✔
820

821
    this.context.info("{}={} has been set in {}", name, edition, settingsVariables.getSource());
19✔
822
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
823
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
824
      this.context.warning("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name,
13✔
825
          declaringVariables.getSource());
2✔
826
    }
827
    if (hint) {
2!
828
      this.context.info("To install that edition call the following command:");
4✔
829
      this.context.info("ide install {}", this.tool);
11✔
830
    }
831
  }
1✔
832

833
  /**
834
   * Runs the tool's help command to provide the user with usage information.
835
   */
836
  @Override
837
  public void printHelp(NlsBundle bundle) {
838

839
    super.printHelp(bundle);
3✔
840
    String toolHelpArgs = getToolHelpArguments();
3✔
841
    if (toolHelpArgs != null && getInstalledVersion() != null) {
5!
842
      ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.LOG_WARNING)
6✔
843
          .executable(Path.of(getBinaryName())).addArgs(toolHelpArgs);
13✔
844
      pc.run(ProcessMode.DEFAULT);
4✔
845
    }
846
  }
1✔
847

848
  /**
849
   * @return the tool's specific help command. Usually help, --help or -h. Return null if not applicable.
850
   */
851
  public String getToolHelpArguments() {
852

853
    return null;
×
854
  }
855

856
  /**
857
   * Creates a start script for the tool using the tool name.
858
   *
859
   * @param targetDir the {@link Path} of the installation where to create the script. If a "bin" sub-folder is present, the script will be created there
860
   *     instead.
861
   * @param binary name of the binary to execute from the start script.
862
   */
863
  protected void createStartScript(Path targetDir, String binary) {
864

865
    createStartScript(targetDir, binary, false);
×
866
  }
×
867

868
  /**
869
   * Creates a start script for the tool using the tool name.
870
   *
871
   * @param targetDir the {@link Path} of the installation where to create the script. If a "bin" sub-folder is present, the script will be created there
872
   *     instead.
873
   * @param binary name of the binary to execute from the start script.
874
   * @param background {@code true} to run the {@code binary} in background, {@code false} otherwise (foreground).
875
   */
876
  protected void createStartScript(Path targetDir, String binary, boolean background) {
877

878
    Path binFolder = targetDir.resolve("bin");
×
879
    if (!Files.exists(binFolder)) {
×
880
      if (this.context.getSystemInfo().isMac()) {
×
881
        MacOsHelper macOsHelper = getMacOsHelper();
×
882
        Path appDir = macOsHelper.findAppDir(targetDir);
×
883
        binFolder = macOsHelper.findLinkDir(appDir, binary);
×
884
      } else {
×
885
        binFolder = targetDir;
×
886
      }
887
      assert (Files.exists(binFolder));
×
888
    }
889
    Path bashFile = binFolder.resolve(getName());
×
890
    String bashFileContentStart = "#!/usr/bin/env bash\n\"$(dirname \"$0\")/";
×
891
    String bashFileContentEnd = "\" $@";
×
892
    if (background) {
×
893
      bashFileContentEnd += " &";
×
894
    }
895
    try {
896
      Files.writeString(bashFile, bashFileContentStart + binary + bashFileContentEnd);
×
897
    } catch (IOException e) {
×
898
      throw new RuntimeException(e);
×
899
    }
×
900
    assert (Files.exists(bashFile));
×
901
    context.getFileAccess().makeExecutable(bashFile);
×
902
  }
×
903

904
  @Override
905
  public void reset() {
906
    super.reset();
2✔
907
    this.executionDirectory = null;
3✔
908
  }
1✔
909

910
  /**
911
   * @param step the {@link Step} to get {@link Step#asSuccess() success logger} from. May be {@code null}.
912
   * @return the {@link IdeSubLogger} from {@link Step#asSuccess()} or {@link IdeContext#success()} as fallback.
913
   */
914
  protected IdeSubLogger asSuccess(Step step) {
915

916
    if (step == null) {
2!
917
      return this.context.success();
4✔
918
    } else {
919
      return step.asSuccess();
×
920
    }
921
  }
922

923

924
  /**
925
   * @param step the {@link Step} to get {@link Step#asError() error logger} from. May be {@code null}.
926
   * @return the {@link IdeSubLogger} from {@link Step#asError()} or {@link IdeContext#error()} as fallback.
927
   */
928
  protected IdeSubLogger asError(Step step) {
929

930
    if (step == null) {
×
931
      return this.context.error();
×
932
    } else {
933
      return step.asError();
×
934
    }
935
  }
936
}
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