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

devonfw / IDEasy / 19548176196

20 Nov 2025 07:00PM UTC coverage: 69.114% (+0.2%) from 68.924%
19548176196

Pull #1593

github

web-flow
Merge add44c871 into ffcb5d97f
Pull Request #1593: #1144: #1145: CVE warnings and suggestions

3597 of 5699 branches covered (63.12%)

Branch coverage included in aggregate %.

9357 of 13044 relevant lines covered (71.73%)

3.15 hits per line

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

73.17
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 getToolWithEdition() {
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
    if (toolVersion != null) {
2!
185
      throw new UnsupportedOperationException("Not implemented yet");
×
186
    }
187
    ProcessContext pc = this.context.newProcess().errorHandling(errorHandling);
6✔
188
    install(true, getConfiguredVersion(), pc, null);
8✔
189
    return runTool(processMode, errorHandling, pc, args);
7✔
190
  }
191

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

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

209
  /**
210
   * @param pc the {@link ProcessContext}.
211
   * @param processMode the {@link ProcessMode}.
212
   * @param errorHandling the {@link ProcessErrorHandling}.
213
   */
214
  protected void configureToolBinary(ProcessContext pc, ProcessMode processMode, ProcessErrorHandling errorHandling) {
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 errorHandling the {@link ProcessErrorHandling}.
223
   * @param args the command-line arguments to {@link ProcessContext#addArgs(Object...) add}.
224
   */
225
  protected void configureToolArgs(ProcessContext pc, ProcessMode processMode, ProcessErrorHandling errorHandling, String... args) {
226

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

230
  /**
231
   * Creates a new {@link ProcessContext} from the given executable with the provided arguments attached.
232
   *
233
   * @param binaryPath path to the binary executable for this process
234
   * @param args the command-line arguments for this process
235
   * @return {@link ProcessContext}
236
   */
237
  protected ProcessContext createProcessContext(Path binaryPath, String... args) {
238

239
    return this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_ERR).executable(binaryPath).addArgs(args);
×
240
  }
241

242
  /**
243
   * Installs or updates the managed {@link #getName() tool}.
244
   *
245
   * @return the {@link ToolInstallation}.
246
   */
247
  public ToolInstallation install() {
248

249
    return install(true);
4✔
250
  }
251

252
  /**
253
   * Performs the installation of the {@link #getName() tool} managed by this {@link com.devonfw.tools.ide.commandlet.Commandlet}.
254
   *
255
   * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
256
   * @return the {@link ToolInstallation}.
257
   */
258
  public ToolInstallation install(boolean silent) {
259
    return install(silent, getConfiguredVersion());
6✔
260
  }
261

262
  /**
263
   *
264
   * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
265
   * @param configuredVersion the version to install, typically {@link #getConfiguredVersion()}.
266
   * @return the {@link ToolInstallation}.
267
   */
268
  public ToolInstallation install(boolean silent, VersionIdentifier configuredVersion) {
269
    ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_CLI);
6✔
270
    return install(silent, configuredVersion, pc, null);
7✔
271
  }
272

273

274
  /**
275
   * Installs or updates the managed {@link #getName() tool}.
276
   *
277
   * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
278
   * @param processContext the {@link ProcessContext} used to
279
   *     {@link LocalToolCommandlet#setEnvironment(EnvironmentContext, ToolInstallation, boolean) configure environment variables}.
280
   * @param step the {@link Step} to track the installation. May be {@code null} to fail with {@link Exception} on error.
281
   * @param configuredVersion the version to install, typically {@link #getConfiguredVersion()}.
282
   * @return the {@link ToolInstallation}.
283
   */
284
  public abstract ToolInstallation install(boolean silent, VersionIdentifier configuredVersion, ProcessContext processContext, Step step);
285

286
  /**
287
   * This method is called after a tool was requested to be installed or updated.
288
   *
289
   * @param newlyInstalled {@code true} if the tool was installed or updated (at least link to software folder was created/updated), {@code false} otherwise
290
   *     (configured version was already installed and nothing changed).
291
   * @param pc the {@link ProcessContext} to use.
292
   */
293
  protected void postInstall(boolean newlyInstalled, ProcessContext pc) {
294

295
    if (newlyInstalled) {
2✔
296
      postInstall();
2✔
297
    }
298
  }
1✔
299

300
  /**
301
   * This method is called after the tool has been newly installed or updated to a new version.
302
   */
303
  protected void postInstall() {
304

305
    // nothing to do by default
306
  }
1✔
307

308
  protected abstract Path getInstallationPath(String edition, VersionIdentifier resolvedVersion);
309

310
  /**
311
   * @param edition the {@link #getConfiguredEdition() edition}.
312
   * @param installedVersion the {@link #getConfiguredVersion() version}.
313
   * @param environmentContext the {@link EnvironmentContext}.
314
   * @param extraInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
315
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
316
   * @return the {@link ToolInstallation}.
317
   */
318
  protected ToolInstallation createExistingToolInstallation(String edition, VersionIdentifier installedVersion, EnvironmentContext environmentContext,
319
      boolean extraInstallation) {
320

321
    Path installationPath = getInstallationPath(edition, installedVersion);
5✔
322
    return createToolInstallation(installationPath, installedVersion, false, environmentContext, extraInstallation);
8✔
323
  }
324

325
  /**
326
   * @param rootDir the {@link ToolInstallation#rootDir() top-level installation directory}.
327
   * @param version the installed {@link VersionIdentifier}.
328
   * @param newInstallation {@link ToolInstallation#newInstallation() new installation} flag.
329
   * @param environmentContext the {@link EnvironmentContext}.
330
   * @param extraInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
331
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
332
   * @return the {@link ToolInstallation}.
333
   */
334
  protected ToolInstallation createToolInstallation(Path rootDir, VersionIdentifier version, boolean newInstallation,
335
      EnvironmentContext environmentContext, boolean extraInstallation) {
336

337
    Path linkDir = rootDir;
2✔
338
    Path binDir = rootDir;
2✔
339
    if (rootDir != null) {
2✔
340
      linkDir = getMacOsHelper().findLinkDir(rootDir, getBinaryName());
7✔
341
      binDir = this.context.getFileAccess().getBinPath(linkDir);
6✔
342
    }
343
    return createToolInstallation(rootDir, linkDir, binDir, version, newInstallation, environmentContext, extraInstallation);
10✔
344
  }
345

346
  /**
347
   * @param rootDir the {@link ToolInstallation#rootDir() top-level installation directory}.
348
   * @param linkDir the {@link ToolInstallation#linkDir() link directory}.
349
   * @param binDir the {@link ToolInstallation#binDir() bin directory}.
350
   * @param version the installed {@link VersionIdentifier}.
351
   * @param newInstallation {@link ToolInstallation#newInstallation() new installation} flag.
352
   * @param environmentContext the {@link EnvironmentContext}.
353
   * @param extraInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
354
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
355
   * @return the {@link ToolInstallation}.
356
   */
357
  protected ToolInstallation createToolInstallation(Path rootDir, Path linkDir, Path binDir, VersionIdentifier version, boolean newInstallation,
358
      EnvironmentContext environmentContext, boolean extraInstallation) {
359

360
    if (linkDir != rootDir) {
3✔
361
      assert (!linkDir.equals(rootDir));
5!
362
      Path toolVersionFile = rootDir.resolve(IdeContext.FILE_SOFTWARE_VERSION);
4✔
363
      if (Files.exists(toolVersionFile)) {
5!
364
        this.context.getFileAccess().copy(toolVersionFile, linkDir, FileCopyMode.COPY_FILE_OVERRIDE);
7✔
365
      }
366
    }
367
    ToolInstallation toolInstallation = new ToolInstallation(rootDir, linkDir, binDir, version, newInstallation);
9✔
368
    setEnvironment(environmentContext, toolInstallation, extraInstallation);
5✔
369
    return toolInstallation;
2✔
370
  }
371

372
  /**
373
   * Called if tool is already installed and detected before actual {@link #install() install} method was called.
374
   *
375
   * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
376
   * @param toolEdition the installed {@link ToolEdition}.
377
   * @param installedVersion the installed {@link VersionIdentifier}.
378
   * @param pc the {@link ProcessContext} to use.
379
   * @param extraInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
380
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
381
   * @return the {@link ToolInstallation}.
382
   */
383
  protected ToolInstallation toolAlreadyInstalled(boolean silent, ToolEdition toolEdition, VersionIdentifier installedVersion, ProcessContext pc,
384
      boolean extraInstallation) {
385

386
    logToolAlreadyInstalled(silent, toolEdition, installedVersion);
5✔
387
    cveCheck(toolEdition, installedVersion, null, true);
7✔
388
    postInstall(false, pc);
4✔
389
    return createExistingToolInstallation(toolEdition.edition(), installedVersion, pc, extraInstallation);
8✔
390
  }
391

392
  /**
393
   * Called if tool is already installed and detected before actual {@link #install() install} method was called.
394
   *
395
   * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
396
   * @param toolEdition the installed {@link ToolEdition}.
397
   * @param installedVersion the installed {@link VersionIdentifier}.
398
   * @param pc the {@link ProcessContext} to use.
399
   * @param extraInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
400
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
401
   * @return the {@link ToolInstallation}.
402
   */
403
  protected ToolInstallation toolAlreadyInstalled(boolean silent, Path binPath, ToolEdition toolEdition, VersionIdentifier installedVersion, ProcessContext pc,
404
      boolean extraInstallation) {
405

406
    logToolAlreadyInstalled(silent, toolEdition, installedVersion);
×
407
    cveCheck(toolEdition, installedVersion, null, true);
×
408
    postInstall(false, pc);
×
409
    Path rootPath = this.context.getFileAccess().getBinParentPath(binPath);
×
410
    return createToolInstallation(rootPath, rootPath, binPath, installedVersion, false, pc, extraInstallation);
×
411
  }
412

413
  /**
414
   * Log that the tool is already installed.
415
   *
416
   * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
417
   * @param toolEdition the installed {@link ToolEdition}.
418
   * @param installedVersion the installed {@link VersionIdentifier}.
419
   */
420
  protected void logToolAlreadyInstalled(boolean silent, ToolEdition toolEdition, VersionIdentifier installedVersion) {
421
    IdeSubLogger logger;
422
    if (silent) {
2✔
423
      logger = this.context.debug();
5✔
424
    } else {
425
      logger = this.context.info();
4✔
426
    }
427
    logger.log("Version {} of tool {} is already installed", installedVersion, toolEdition);
14✔
428
  }
1✔
429

430
  /**
431
   * Method to get the home path of the given {@link ToolInstallation}.
432
   *
433
   * @param toolInstallation the {@link ToolInstallation}.
434
   * @return the Path to the home of the tool
435
   */
436
  protected Path getToolHomePath(ToolInstallation toolInstallation) {
437
    return toolInstallation.linkDir();
3✔
438
  }
439

440
  /**
441
   * Method to set environment variables for the process context.
442
   *
443
   * @param environmentContext the {@link EnvironmentContext} where to {@link EnvironmentContext#withEnvVar(String, String) set environment variables} for
444
   *     this tool.
445
   * @param toolInstallation the {@link ToolInstallation}.
446
   * @param extraInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
447
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
448
   */
449
  public void setEnvironment(EnvironmentContext environmentContext, ToolInstallation toolInstallation, boolean extraInstallation) {
450

451
    String pathVariable = EnvironmentVariables.getToolVariablePrefix(this.tool) + "_HOME";
5✔
452
    Path toolHomePath = getToolHomePath(toolInstallation);
4✔
453
    if (toolHomePath != null) {
2✔
454
      environmentContext.withEnvVar(pathVariable, toolHomePath.toString());
6✔
455
    }
456
    if (extraInstallation) {
2✔
457
      environmentContext.withPathEntry(toolInstallation.binDir());
5✔
458
    }
459
  }
1✔
460

461
  /**
462
   * @return {@code true} to extract (unpack) the downloaded binary file, {@code false} otherwise.
463
   */
464
  protected boolean isExtract() {
465

466
    return true;
2✔
467
  }
468

469
  /**
470
   * 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
471
   * something better, we will suggest this to the user and ask him to make his choice.
472
   *
473
   * @param toolEdition the {@link ToolEdition}.
474
   * @param resolvedVersion the resolved {@link #getConfiguredEdition() version}.
475
   * @param allowedVersions a {@link GenericVersionRange} that defines which versions are allowed to consider.
476
   * @param skipSuggestions {@code true} to skip suggestions, {@code false} otherwise (try to find alternative suggestions and ask the user).
477
   * @return the {@link VersionIdentifier} to install. If there were {@link Cve}s found and better versions available the user made this choice.
478
   */
479
  protected VersionIdentifier cveCheck(ToolEdition toolEdition, VersionIdentifier resolvedVersion, GenericVersionRange allowedVersions,
480
      boolean skipSuggestions) {
481

482
    ToolSecurity toolSecurity = this.context.getDefaultToolRepository().findSecurity(this.tool, toolEdition.edition());
9✔
483
    double minSeverity = IdeVariables.CVE_MIN_SEVERITY.get(context);
7✔
484
    Collection<Cve> issues = toolSecurity.findCves(resolvedVersion, this.context, minSeverity);
7✔
485
    ToolVersionChoice currentChoice = ToolVersionChoice.ofCurrent(resolvedVersion, issues);
4✔
486
    if (logCvesAndReturnTrueForNone(toolEdition, resolvedVersion, currentChoice.option(), issues)) {
8✔
487
      return resolvedVersion;
2✔
488
    }
489
    if (skipSuggestions) {
2!
490
      // currently for a transitive dependency it does not make sense to suggest alternative versions, since the choice is not stored anywhere,
491
      // 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
492
      // (e.g. upgrade the tool with the dependency that is causing this).
493
      this.context.interaction("Please run 'ide install {}' to check for update suggestions!", this.tool);
×
494
      return resolvedVersion;
×
495
    }
496
    double currentSeveritySum = Cve.severitySum(issues);
3✔
497
    ToolVersionChoice latest = null;
2✔
498
    ToolVersionChoice nearest = null;
2✔
499
    List<VersionIdentifier> toolVersions = getVersions();
3✔
500
    double latestSeveritySum = currentSeveritySum;
2✔
501
    double nearestSeveritySum = currentSeveritySum;
2✔
502
    for (VersionIdentifier version : toolVersions) {
10✔
503
      if (allowedVersions == null || allowedVersions.contains(version)) {
6!
504
        issues = toolSecurity.findCves(version, this.context, minSeverity);
7✔
505
        double newSeveritySum = Cve.severitySum(issues);
3✔
506
        if (newSeveritySum < latestSeveritySum) {
4✔
507
          // we found a better/safer version
508
          if (version.isGreater(resolvedVersion)) {
4!
509
            latest = ToolVersionChoice.ofLatest(version, issues);
4✔
510
            nearest = null;
2✔
511
            latestSeveritySum = newSeveritySum;
3✔
512
          } else {
513
            // latest = null;
514
            nearest = ToolVersionChoice.ofNearest(version, issues);
×
515
            nearestSeveritySum = newSeveritySum;
×
516
          }
517
        } else if (newSeveritySum < nearestSeveritySum) {
4✔
518
          if (version.isGreater(resolvedVersion)) {
4!
519
            nearest = ToolVersionChoice.ofNearest(version, issues);
×
520
          } else if (nearest == null) {
2!
521
            nearest = ToolVersionChoice.ofNearest(version, issues);
4✔
522
          }
523
          nearestSeveritySum = newSeveritySum;
2✔
524
        }
525
      }
526
    }
1✔
527
    if ((latest == null) && (nearest == null)) {
2!
528
      this.context.warning(
×
529
          "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.");
530
    }
531
    List<ToolVersionChoice> choices = new ArrayList<>();
4✔
532
    choices.add(currentChoice);
4✔
533
    List<String> skipCveFixTools = IdeVariables.SKIP_CVE_FIX.get(this.context);
6✔
534
    boolean addSuggestions = !skipCveFixTools.contains(this.tool);
8!
535
    if (nearest != null) {
2!
536
      if (addSuggestions) {
2!
537
        choices.add(nearest);
4✔
538
      }
539
      logCvesAndReturnTrueForNone(toolEdition, nearest.version(), nearest.option(), nearest.issues());
10✔
540
    }
541
    if (latest != null) {
2!
542
      if (addSuggestions) {
2!
543
        choices.add(latest);
4✔
544
      }
545
      logCvesAndReturnTrueForNone(toolEdition, latest.version(), latest.option(), latest.issues());
10✔
546
    }
547
    ToolVersionChoice[] choicesArray = choices.toArray(ToolVersionChoice[]::new);
8✔
548
    this.context.warning(
4✔
549
        "Please note that by selecting an unsafe version to install, you accept the risk to be attacked.");
550
    ToolVersionChoice answer = this.context.question(choicesArray, "Which version do you want to install?");
9✔
551
    return answer.version();
3✔
552
  }
553

554
  private boolean logCvesAndReturnTrueForNone(ToolEdition toolEdition, VersionIdentifier version, String option, Collection<Cve> issues) {
555
    if (issues.isEmpty()) {
3✔
556
      this.context.info("No CVEs found for {} version {} of tool {}.", option, version, toolEdition);
18✔
557
      return true;
2✔
558
    }
559
    this.context.warning("For {} version {} of tool {} we found {} CVE(s):", option, version, toolEdition, issues.size());
24✔
560
    for (Cve cve : issues) {
10✔
561
      logCve(cve);
3✔
562
    }
1✔
563
    return false;
2✔
564
  }
565

566
  private void logCve(Cve cve) {
567

568
    this.context.warning("{} with severity {} and affected versions: {} ", cve.id(), cve.severity(), cve.versions());
22✔
569
    this.context.warning("https://nvd.nist.gov/vuln/detail/" + cve.id());
6✔
570
    this.context.info("");
4✔
571
  }
1✔
572

573
  /**
574
   * @return the {@link MacOsHelper} instance.
575
   */
576
  protected MacOsHelper getMacOsHelper() {
577

578
    if (this.macOsHelper == null) {
3✔
579
      this.macOsHelper = new MacOsHelper(this.context);
7✔
580
    }
581
    return this.macOsHelper;
3✔
582
  }
583

584
  /**
585
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
586
   */
587
  public abstract VersionIdentifier getInstalledVersion();
588

589
  /**
590
   * @return {@code true} if this tool is installed, {@code false} otherwise.
591
   */
592
  public boolean isInstalled() {
593

594
    return getInstalledVersion() != null;
7✔
595
  }
596

597
  /**
598
   * @return the installed edition of this tool or {@code null} if not installed.
599
   */
600
  public abstract String getInstalledEdition();
601

602
  /**
603
   * Uninstalls the {@link #getName() tool}.
604
   */
605
  public abstract void uninstall();
606

607
  /**
608
   * @return the {@link ToolRepository}.
609
   */
610
  public ToolRepository getToolRepository() {
611

612
    return this.context.getDefaultToolRepository();
4✔
613
  }
614

615
  /**
616
   * List the available editions of this tool.
617
   */
618
  public void listEditions() {
619

620
    List<String> editions = getToolRepository().getSortedEditions(getName());
6✔
621
    for (String edition : editions) {
10✔
622
      this.context.info(edition);
4✔
623
    }
1✔
624
  }
1✔
625

626
  /**
627
   * List the available versions of this tool.
628
   */
629
  public void listVersions() {
630

631
    List<VersionIdentifier> versions = getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
632
    for (VersionIdentifier vi : versions) {
10✔
633
      this.context.info(vi.toString());
5✔
634
    }
1✔
635
  }
1✔
636

637
  /**
638
   * @return the {@link com.devonfw.tools.ide.tool.repository.DefaultToolRepository#getSortedVersions(String, String, ToolCommandlet) sorted versions} of this
639
   *     tool.
640
   */
641
  public List<VersionIdentifier> getVersions() {
642
    return getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
643
  }
644

645
  /**
646
   * Sets the tool version in the environment variable configuration file.
647
   *
648
   * @param version the version (pattern) to set.
649
   */
650
  public void setVersion(String version) {
651

652
    if ((version == null) || version.isBlank()) {
×
653
      throw new IllegalStateException("Version has to be specified!");
×
654
    }
655
    VersionIdentifier configuredVersion = VersionIdentifier.of(version);
×
656
    if (!configuredVersion.isPattern() && !configuredVersion.isValid()) {
×
657
      this.context.warning("Version {} seems to be invalid", version);
×
658
    }
659
    setVersion(configuredVersion, true);
×
660
  }
×
661

662
  /**
663
   * Sets the tool version in the environment variable configuration file.
664
   *
665
   * @param version the version to set. May also be a {@link VersionIdentifier#isPattern() version pattern}.
666
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
667
   */
668
  public void setVersion(VersionIdentifier version, boolean hint) {
669

670
    setVersion(version, hint, null);
5✔
671
  }
1✔
672

673
  /**
674
   * Sets the tool version in the environment variable configuration file.
675
   *
676
   * @param version the version to set. May also be a {@link VersionIdentifier#isPattern() version pattern}.
677
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
678
   * @param destination - the destination for the property to be set
679
   */
680
  public void setVersion(VersionIdentifier version, boolean hint, EnvironmentVariablesFiles destination) {
681

682
    String edition = getConfiguredEdition();
3✔
683
    ToolRepository toolRepository = getToolRepository();
3✔
684

685
    EnvironmentVariables variables = this.context.getVariables();
4✔
686
    if (destination == null) {
2✔
687
      //use default location
688
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
689
    }
690
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
691
    String name = EnvironmentVariables.getToolVersionVariable(this.tool);
4✔
692

693
    toolRepository.resolveVersion(this.tool, edition, version, this); // verify that the version actually exists
8✔
694
    settingsVariables.set(name, version.toString(), false);
7✔
695
    settingsVariables.save();
2✔
696
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
697
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
698
      this.context.warning("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name,
13✔
699
          declaringVariables.getSource());
2✔
700
    }
701
    if (hint) {
2✔
702
      this.context.info("To install that version call the following command:");
4✔
703
      this.context.info("ide install {}", this.tool);
11✔
704
    }
705
  }
1✔
706

707
  /**
708
   * Sets the tool edition in the environment variable configuration file.
709
   *
710
   * @param edition the edition to set.
711
   */
712
  public void setEdition(String edition) {
713

714
    setEdition(edition, true);
4✔
715
  }
1✔
716

717
  /**
718
   * Sets the tool edition in the environment variable configuration file.
719
   *
720
   * @param edition the edition to set
721
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
722
   */
723
  public void setEdition(String edition, boolean hint) {
724

725
    setEdition(edition, hint, null);
5✔
726
  }
1✔
727

728
  /**
729
   * Sets the tool edition in the environment variable configuration file.
730
   *
731
   * @param edition the edition to set
732
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
733
   * @param destination - the destination for the property to be set
734
   */
735
  public void setEdition(String edition, boolean hint, EnvironmentVariablesFiles destination) {
736

737
    if ((edition == null) || edition.isBlank()) {
5!
738
      throw new IllegalStateException("Edition has to be specified!");
×
739
    }
740

741
    if (destination == null) {
2✔
742
      //use default location
743
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
744
    }
745

746
    if (!getToolRepository().getSortedEditions(this.tool).contains(edition)) {
8✔
747
      this.context.warning("Edition {} seems to be invalid", edition);
10✔
748
    }
749
    EnvironmentVariables variables = this.context.getVariables();
4✔
750
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
751
    String name = EnvironmentVariables.getToolEditionVariable(this.tool);
4✔
752
    settingsVariables.set(name, edition, false);
6✔
753
    settingsVariables.save();
2✔
754

755
    this.context.info("{}={} has been set in {}", name, edition, settingsVariables.getSource());
19✔
756
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
757
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
758
      this.context.warning("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name,
13✔
759
          declaringVariables.getSource());
2✔
760
    }
761
    if (hint) {
2!
762
      this.context.info("To install that edition call the following command:");
4✔
763
      this.context.info("ide install {}", this.tool);
11✔
764
    }
765
  }
1✔
766

767
  /**
768
   * Runs the tool's help command to provide the user with usage information.
769
   */
770
  @Override
771
  public void printHelp(NlsBundle bundle) {
772

773
    super.printHelp(bundle);
3✔
774
    String toolHelpArgs = getToolHelpArguments();
3✔
775
    if (toolHelpArgs != null && getInstalledVersion() != null) {
5!
776
      ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.LOG_WARNING)
6✔
777
          .executable(Path.of(getBinaryName())).addArgs(toolHelpArgs);
13✔
778
      pc.run(ProcessMode.DEFAULT);
4✔
779
    }
780
  }
1✔
781

782
  /**
783
   * @return the tool's specific help command. Usually help, --help or -h. Return null if not applicable.
784
   */
785
  public String getToolHelpArguments() {
786

787
    return null;
×
788
  }
789

790
  /**
791
   * Creates a start script for the tool using the tool name.
792
   *
793
   * @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
794
   *     instead.
795
   * @param binary name of the binary to execute from the start script.
796
   */
797
  protected void createStartScript(Path targetDir, String binary) {
798

799
    createStartScript(targetDir, binary, false);
×
800
  }
×
801

802
  /**
803
   * Creates a start script for the tool using the tool name.
804
   *
805
   * @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
806
   *     instead.
807
   * @param binary name of the binary to execute from the start script.
808
   * @param background {@code true} to run the {@code binary} in background, {@code false} otherwise (foreground).
809
   */
810
  protected void createStartScript(Path targetDir, String binary, boolean background) {
811

812
    Path binFolder = targetDir.resolve("bin");
×
813
    if (!Files.exists(binFolder)) {
×
814
      if (this.context.getSystemInfo().isMac()) {
×
815
        MacOsHelper macOsHelper = getMacOsHelper();
×
816
        Path appDir = macOsHelper.findAppDir(targetDir);
×
817
        binFolder = macOsHelper.findLinkDir(appDir, binary);
×
818
      } else {
×
819
        binFolder = targetDir;
×
820
      }
821
      assert (Files.exists(binFolder));
×
822
    }
823
    Path bashFile = binFolder.resolve(getName());
×
824
    String bashFileContentStart = "#!/usr/bin/env bash\n\"$(dirname \"$0\")/";
×
825
    String bashFileContentEnd = "\" $@";
×
826
    if (background) {
×
827
      bashFileContentEnd += " &";
×
828
    }
829
    try {
830
      Files.writeString(bashFile, bashFileContentStart + binary + bashFileContentEnd);
×
831
    } catch (IOException e) {
×
832
      throw new RuntimeException(e);
×
833
    }
×
834
    assert (Files.exists(bashFile));
×
835
    context.getFileAccess().makeExecutable(bashFile);
×
836
  }
×
837

838
  @Override
839
  public void reset() {
840
    super.reset();
2✔
841
    this.executionDirectory = null;
3✔
842
  }
1✔
843

844
  /**
845
   * @param step the {@link Step} to get {@link Step#asSuccess() success logger} from. May be {@code null}.
846
   * @return the {@link IdeSubLogger} from {@link Step#asSuccess()} or {@link IdeContext#success()} as fallback.
847
   */
848
  protected IdeSubLogger asSuccess(Step step) {
849

850
    if (step == null) {
2!
851
      return this.context.success();
4✔
852
    } else {
853
      return step.asSuccess();
×
854
    }
855
  }
856

857

858
  /**
859
   * @param step the {@link Step} to get {@link Step#asError() error logger} from. May be {@code null}.
860
   * @return the {@link IdeSubLogger} from {@link Step#asError()} or {@link IdeContext#error()} as fallback.
861
   */
862
  protected IdeSubLogger asError(Step step) {
863

864
    if (step == null) {
×
865
      return this.context.error();
×
866
    } else {
867
      return step.asError();
×
868
    }
869
  }
870
}
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