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

devonfw / IDEasy / 19650569677

24 Nov 2025 09:57PM UTC coverage: 69.156% (+0.1%) from 69.024%
19650569677

Pull #1593

github

web-flow
Merge b593a72bf into 0e1be7b6c
Pull Request #1593: #1144: #1145: CVE warnings and suggestions

3613 of 5721 branches covered (63.15%)

Branch coverage included in aggregate %.

9387 of 13077 relevant lines covered (71.78%)

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
  /**
309
   * @param edition the {@link #getInstalledEdition() edition}.
310
   * @param version the {@link #getInstalledVersion() version}.
311
   * @return the {@link Path} where this tool is installed (physically) or {@code null} if not available.
312
   */
313
  protected abstract Path getInstallationPath(String edition, VersionIdentifier version);
314

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

326
    Path installationPath = getInstallationPath(edition, installedVersion);
5✔
327
    return createToolInstallation(installationPath, installedVersion, false, environmentContext, extraInstallation);
8✔
328
  }
329

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

342
    Path linkDir = rootDir;
2✔
343
    Path binDir = rootDir;
2✔
344
    if (rootDir != null) {
2✔
345
      // on MacOS applications have a very strange structure - see JavaDoc of findLinkDir and ToolInstallation.linkDir for details.
346
      linkDir = getMacOsHelper().findLinkDir(rootDir, getBinaryName());
7✔
347
      binDir = this.context.getFileAccess().getBinPath(linkDir);
6✔
348
    }
349
    return createToolInstallation(rootDir, linkDir, binDir, version, newInstallation, environmentContext, extraInstallation);
10✔
350
  }
351

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

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

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

392
    logToolAlreadyInstalled(silent, toolEdition, installedVersion);
5✔
393
    cveCheck(toolEdition, installedVersion, null, true);
7✔
394
    postInstall(false, pc);
4✔
395
    return createExistingToolInstallation(toolEdition.edition(), installedVersion, pc, extraInstallation);
8✔
396
  }
397

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

412
    logToolAlreadyInstalled(silent, toolEdition, installedVersion);
×
413
    cveCheck(toolEdition, installedVersion, null, true);
×
414
    postInstall(false, pc);
×
415
    Path rootPath = this.context.getFileAccess().getBinParentPath(binPath);
×
416
    return createToolInstallation(rootPath, rootPath, binPath, installedVersion, false, pc, extraInstallation);
×
417
  }
418

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

436
  /**
437
   * Method to get the home path of the given {@link ToolInstallation}.
438
   *
439
   * @param toolInstallation the {@link ToolInstallation}.
440
   * @return the Path to the home of the tool
441
   */
442
  protected Path getToolHomePath(ToolInstallation toolInstallation) {
443
    return toolInstallation.linkDir();
3✔
444
  }
445

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

457
    String pathVariable = EnvironmentVariables.getToolVariablePrefix(this.tool) + "_HOME";
5✔
458
    Path toolHomePath = getToolHomePath(toolInstallation);
4✔
459
    if (toolHomePath != null) {
2✔
460
      environmentContext.withEnvVar(pathVariable, toolHomePath.toString());
6✔
461
    }
462
    if (extraInstallation) {
2✔
463
      environmentContext.withPathEntry(toolInstallation.binDir());
5✔
464
    }
465
  }
1✔
466

467
  /**
468
   * @return {@code true} to extract (unpack) the downloaded binary file, {@code false} otherwise.
469
   */
470
  protected boolean isExtract() {
471

472
    return true;
2✔
473
  }
474

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

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

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

572
  private void logCve(Cve cve) {
573

574
    this.context.warning("{} with severity {} and affected versions: {} ", cve.id(), cve.severity(), cve.versions());
22✔
575
    this.context.warning("https://nvd.nist.gov/vuln/detail/" + cve.id());
6✔
576
    this.context.info("");
4✔
577
  }
1✔
578

579
  /**
580
   * @return the {@link MacOsHelper} instance.
581
   */
582
  protected MacOsHelper getMacOsHelper() {
583

584
    if (this.macOsHelper == null) {
3✔
585
      this.macOsHelper = new MacOsHelper(this.context);
7✔
586
    }
587
    return this.macOsHelper;
3✔
588
  }
589

590
  /**
591
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
592
   */
593
  public abstract VersionIdentifier getInstalledVersion();
594

595
  /**
596
   * @return {@code true} if this tool is installed, {@code false} otherwise.
597
   */
598
  public boolean isInstalled() {
599

600
    return getInstalledVersion() != null;
7✔
601
  }
602

603
  /**
604
   * @return the installed edition of this tool or {@code null} if not installed.
605
   */
606
  public abstract String getInstalledEdition();
607

608
  /**
609
   * Uninstalls the {@link #getName() tool}.
610
   */
611
  public abstract void uninstall();
612

613
  /**
614
   * @return the {@link ToolRepository}.
615
   */
616
  public ToolRepository getToolRepository() {
617

618
    return this.context.getDefaultToolRepository();
4✔
619
  }
620

621
  /**
622
   * List the available editions of this tool.
623
   */
624
  public void listEditions() {
625

626
    List<String> editions = getToolRepository().getSortedEditions(getName());
6✔
627
    for (String edition : editions) {
10✔
628
      this.context.info(edition);
4✔
629
    }
1✔
630
  }
1✔
631

632
  /**
633
   * List the available versions of this tool.
634
   */
635
  public void listVersions() {
636

637
    List<VersionIdentifier> versions = getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
638
    for (VersionIdentifier vi : versions) {
10✔
639
      this.context.info(vi.toString());
5✔
640
    }
1✔
641
  }
1✔
642

643
  /**
644
   * @return the {@link com.devonfw.tools.ide.tool.repository.DefaultToolRepository#getSortedVersions(String, String, ToolCommandlet) sorted versions} of this
645
   *     tool.
646
   */
647
  public List<VersionIdentifier> getVersions() {
648
    return getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
649
  }
650

651
  /**
652
   * Sets the tool version in the environment variable configuration file.
653
   *
654
   * @param version the version (pattern) to set.
655
   */
656
  public void setVersion(String version) {
657

658
    if ((version == null) || version.isBlank()) {
×
659
      throw new IllegalStateException("Version has to be specified!");
×
660
    }
661
    VersionIdentifier configuredVersion = VersionIdentifier.of(version);
×
662
    if (!configuredVersion.isPattern() && !configuredVersion.isValid()) {
×
663
      this.context.warning("Version {} seems to be invalid", version);
×
664
    }
665
    setVersion(configuredVersion, true);
×
666
  }
×
667

668
  /**
669
   * Sets the tool version in the environment variable configuration file.
670
   *
671
   * @param version the version to set. May also be a {@link VersionIdentifier#isPattern() version pattern}.
672
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
673
   */
674
  public void setVersion(VersionIdentifier version, boolean hint) {
675

676
    setVersion(version, hint, null);
5✔
677
  }
1✔
678

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

688
    String edition = getConfiguredEdition();
3✔
689
    ToolRepository toolRepository = getToolRepository();
3✔
690

691
    EnvironmentVariables variables = this.context.getVariables();
4✔
692
    if (destination == null) {
2✔
693
      //use default location
694
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
695
    }
696
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
697
    String name = EnvironmentVariables.getToolVersionVariable(this.tool);
4✔
698

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

713
  /**
714
   * Sets the tool edition in the environment variable configuration file.
715
   *
716
   * @param edition the edition to set.
717
   */
718
  public void setEdition(String edition) {
719

720
    setEdition(edition, true);
4✔
721
  }
1✔
722

723
  /**
724
   * Sets the tool edition in the environment variable configuration file.
725
   *
726
   * @param edition the edition to set
727
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
728
   */
729
  public void setEdition(String edition, boolean hint) {
730

731
    setEdition(edition, hint, null);
5✔
732
  }
1✔
733

734
  /**
735
   * Sets the tool edition in the environment variable configuration file.
736
   *
737
   * @param edition the edition to set
738
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
739
   * @param destination - the destination for the property to be set
740
   */
741
  public void setEdition(String edition, boolean hint, EnvironmentVariablesFiles destination) {
742

743
    if ((edition == null) || edition.isBlank()) {
5!
744
      throw new IllegalStateException("Edition has to be specified!");
×
745
    }
746

747
    if (destination == null) {
2✔
748
      //use default location
749
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
750
    }
751

752
    if (!getToolRepository().getSortedEditions(this.tool).contains(edition)) {
8✔
753
      this.context.warning("Edition {} seems to be invalid", edition);
10✔
754
    }
755
    EnvironmentVariables variables = this.context.getVariables();
4✔
756
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
757
    String name = EnvironmentVariables.getToolEditionVariable(this.tool);
4✔
758
    settingsVariables.set(name, edition, false);
6✔
759
    settingsVariables.save();
2✔
760

761
    this.context.info("{}={} has been set in {}", name, edition, settingsVariables.getSource());
19✔
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 edition call the following command:");
4✔
769
      this.context.info("ide install {}", this.tool);
11✔
770
    }
771
  }
1✔
772

773
  /**
774
   * Runs the tool's help command to provide the user with usage information.
775
   */
776
  @Override
777
  public void printHelp(NlsBundle bundle) {
778

779
    super.printHelp(bundle);
3✔
780
    String toolHelpArgs = getToolHelpArguments();
3✔
781
    if (toolHelpArgs != null && getInstalledVersion() != null) {
5!
782
      ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.LOG_WARNING)
6✔
783
          .executable(Path.of(getBinaryName())).addArgs(toolHelpArgs);
13✔
784
      pc.run(ProcessMode.DEFAULT);
4✔
785
    }
786
  }
1✔
787

788
  /**
789
   * @return the tool's specific help command. Usually help, --help or -h. Return null if not applicable.
790
   */
791
  public String getToolHelpArguments() {
792

793
    return null;
×
794
  }
795

796
  /**
797
   * Creates a start script for the tool using the tool name.
798
   *
799
   * @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
800
   *     instead.
801
   * @param binary name of the binary to execute from the start script.
802
   */
803
  protected void createStartScript(Path targetDir, String binary) {
804

805
    createStartScript(targetDir, binary, false);
×
806
  }
×
807

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

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

844
  @Override
845
  public void reset() {
846
    super.reset();
2✔
847
    this.executionDirectory = null;
3✔
848
  }
1✔
849

850
  /**
851
   * @param step the {@link Step} to get {@link Step#asSuccess() success logger} from. May be {@code null}.
852
   * @return the {@link IdeSubLogger} from {@link Step#asSuccess()} or {@link IdeContext#success()} as fallback.
853
   */
854
  protected IdeSubLogger asSuccess(Step step) {
855

856
    if (step == null) {
2!
857
      return this.context.success();
4✔
858
    } else {
859
      return step.asSuccess();
×
860
    }
861
  }
862

863

864
  /**
865
   * @param step the {@link Step} to get {@link Step#asError() error logger} from. May be {@code null}.
866
   * @return the {@link IdeSubLogger} from {@link Step#asError()} or {@link IdeContext#error()} as fallback.
867
   */
868
  protected IdeSubLogger asError(Step step) {
869

870
    if (step == null) {
×
871
      return this.context.error();
×
872
    } else {
873
      return step.asError();
×
874
    }
875
  }
876
}
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