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

devonfw / IDEasy / 19877232762

02 Dec 2025 11:45PM UTC coverage: 69.903% (+0.05%) from 69.852%
19877232762

push

github

web-flow
#1636: improved security scoring (#1637)

3857 of 6049 branches covered (63.76%)

Branch coverage included in aggregate %.

9851 of 13561 relevant lines covered (72.64%)

3.15 hits per line

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

76.38
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.List;
8
import java.util.Set;
9

10
import com.devonfw.tools.ide.commandlet.Commandlet;
11
import com.devonfw.tools.ide.common.Tag;
12
import com.devonfw.tools.ide.common.Tags;
13
import com.devonfw.tools.ide.context.IdeContext;
14
import com.devonfw.tools.ide.environment.EnvironmentVariables;
15
import com.devonfw.tools.ide.environment.EnvironmentVariablesFiles;
16
import com.devonfw.tools.ide.io.FileCopyMode;
17
import com.devonfw.tools.ide.log.IdeSubLogger;
18
import com.devonfw.tools.ide.nls.NlsBundle;
19
import com.devonfw.tools.ide.os.MacOsHelper;
20
import com.devonfw.tools.ide.process.EnvironmentContext;
21
import com.devonfw.tools.ide.process.ProcessContext;
22
import com.devonfw.tools.ide.process.ProcessErrorHandling;
23
import com.devonfw.tools.ide.process.ProcessMode;
24
import com.devonfw.tools.ide.process.ProcessResult;
25
import com.devonfw.tools.ide.property.StringProperty;
26
import com.devonfw.tools.ide.security.ToolVersionChoice;
27
import com.devonfw.tools.ide.security.ToolVulnerabilities;
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
    return runTool(request, processMode, args);
6✔
191
  }
192

193
  /**
194
   * Ensures the tool is installed and then runs this tool with the given arguments.
195
   *
196
   * @param request the {@link ToolInstallRequest}.
197
   * @param processMode the {@link ProcessMode}. Should typically be {@link ProcessMode#DEFAULT} or {@link ProcessMode#BACKGROUND}.
198
   * @param args the command-line arguments to run the tool.
199
   * @return the {@link ProcessResult result}.
200
   */
201
  public ProcessResult runTool(ToolInstallRequest request, ProcessMode processMode, String... args) {
202

203
    install(request);
4✔
204
    return runTool(request.getProcessContext(), processMode, args);
7✔
205
  }
206

207
  /**
208
   * @param pc the {@link ProcessContext}.
209
   * @param processMode the {@link ProcessMode}. Should typically be {@link ProcessMode#DEFAULT} or {@link ProcessMode#BACKGROUND}.
210
   * @param args the command-line arguments to run the tool.
211
   * @return the {@link ProcessResult result}.
212
   */
213
  public ProcessResult runTool(ProcessContext pc, ProcessMode processMode, String... args) {
214

215
    if (this.executionDirectory != null) {
3!
216
      pc.directory(this.executionDirectory);
×
217
    }
218
    configureToolBinary(pc, processMode);
4✔
219
    configureToolArgs(pc, processMode, args);
5✔
220
    return pc.run(processMode);
4✔
221
  }
222

223
  /**
224
   * @param pc the {@link ProcessContext}.
225
   * @param processMode the {@link ProcessMode}.
226
   */
227
  protected void configureToolBinary(ProcessContext pc, ProcessMode processMode) {
228

229
    pc.executable(Path.of(getBinaryName()));
8✔
230
  }
1✔
231

232
  /**
233
   * @param pc the {@link ProcessContext}.
234
   * @param processMode the {@link ProcessMode}.
235
   * @param args the command-line arguments to {@link ProcessContext#addArgs(Object...) add}.
236
   */
237
  protected void configureToolArgs(ProcessContext pc, ProcessMode processMode, String... args) {
238

239
    pc.addArgs(args);
4✔
240
  }
1✔
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(new ToolInstallRequest(silent));
7✔
260
  }
261

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

270
    completeRequest(request);
3✔
271
    return doInstall(request);
4✔
272
  }
273

274
  /**
275
   * Performs the installation (install, update, downgrade) of the {@link #getName() tool} managed by this {@link ToolCommandlet}.
276
   *
277
   * @param request the {@link ToolInstallRequest}.
278
   * @return the {@link ToolInstallation}.
279
   */
280
  protected abstract ToolInstallation doInstall(ToolInstallRequest request);
281

282
  /**
283
   * @param request the {@link ToolInstallRequest} to complete (fill values that are currently {@code null}).
284
   */
285
  protected void completeRequest(ToolInstallRequest request) {
286

287
    completeRequestInstalled(request);
3✔
288
    completeRequestRequested(request); // depends on completeRequestInstalled
3✔
289
    completeRequestProcessContext(request);
3✔
290
  }
1✔
291

292
  private void completeRequestProcessContext(ToolInstallRequest request) {
293
    if (request.getProcessContext() == null) {
3✔
294
      ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_CLI);
6✔
295
      request.setProcessContext(pc);
3✔
296
    }
297
  }
1✔
298

299
  private void completeRequestInstalled(ToolInstallRequest request) {
300

301
    ToolEditionAndVersion installedToolVersion = request.getInstalled();
3✔
302
    if (installedToolVersion == null) {
2✔
303
      installedToolVersion = new ToolEditionAndVersion((GenericVersionRange) null);
6✔
304
      request.setInstalled(installedToolVersion);
3✔
305
    }
306
    if (installedToolVersion.getVersion() == null) {
3✔
307
      VersionIdentifier installedVersion = getInstalledVersion();
3✔
308
      if (installedVersion == null) {
2✔
309
        return;
1✔
310
      }
311
      installedToolVersion.setVersion(installedVersion);
3✔
312
    }
313
    if (installedToolVersion.getEdition() == null) {
3✔
314
      installedToolVersion.setEdition(new ToolEdition(this.tool, getInstalledEdition()));
9✔
315
    }
316
    assert installedToolVersion.getResolvedVersion() != null;
4!
317
  }
1✔
318

319
  private void completeRequestRequested(ToolInstallRequest request) {
320

321
    ToolEdition edition;
322
    ToolEditionAndVersion requested = request.getRequested();
3✔
323
    if (requested == null) {
2✔
324
      edition = new ToolEdition(this.tool, getConfiguredEdition());
8✔
325
      requested = new ToolEditionAndVersion(edition);
5✔
326
      request.setRequested(requested);
4✔
327
    } else {
328
      edition = requested.getEdition();
3✔
329
      if (edition == null) {
2✔
330
        edition = new ToolEdition(this.tool, getConfiguredEdition());
8✔
331
        requested.setEdition(edition);
3✔
332
      }
333
    }
334
    GenericVersionRange version = requested.getVersion();
3✔
335
    if (version == null) {
2✔
336
      version = getConfiguredVersion();
3✔
337
      requested.setVersion(version);
3✔
338
    }
339
    VersionIdentifier resolvedVersion = requested.getResolvedVersion();
3✔
340
    if (resolvedVersion == null) {
2✔
341
      if (this.context.isSkipUpdatesMode()) {
4✔
342
        ToolEditionAndVersion installed = request.getInstalled();
3✔
343
        if (installed != null) {
2!
344
          VersionIdentifier installedVersion = installed.getResolvedVersion();
3✔
345
          if (version.contains(installedVersion)) {
4✔
346
            resolvedVersion = installedVersion;
2✔
347
          }
348
        }
349
      }
350
      if (resolvedVersion == null) {
2✔
351
        resolvedVersion = getToolRepository().resolveVersion(this.tool, edition.edition(), version, this);
10✔
352
      }
353
      requested.setResolvedVersion(resolvedVersion);
3✔
354
    }
355
  }
1✔
356

357
  /**
358
   * This method is called after a tool was requested to be installed or updated.
359
   *
360
   * @param request {@code true} the {@link ToolInstallRequest}.
361
   */
362
  protected void postInstall(ToolInstallRequest request) {
363

364
    if (!request.isAlreadyInstalled()) {
3✔
365
      postInstallOnNewInstallation(request);
3✔
366
    }
367
  }
1✔
368

369
  /**
370
   * This method is called after a tool was requested to be installed or updated and a new installation was performed.
371
   *
372
   * @param request {@code true} the {@link ToolInstallRequest}.
373
   */
374
  protected void postInstallOnNewInstallation(ToolInstallRequest request) {
375

376
    // nothing to do by default
377
  }
1✔
378

379
  /**
380
   * @param edition the {@link #getInstalledEdition() edition}.
381
   * @param version the {@link #getInstalledVersion() version}.
382
   * @return the {@link Path} where this tool is installed (physically) or {@code null} if not available.
383
   */
384
  protected abstract Path getInstallationPath(String edition, VersionIdentifier version);
385

386
  /**
387
   * @param request the {@link ToolInstallRequest}.
388
   * @return the existing {@link ToolInstallation}.
389
   */
390
  protected ToolInstallation createExistingToolInstallation(ToolInstallRequest request) {
391

392
    ToolEditionAndVersion installed = request.getInstalled();
3✔
393
    return createExistingToolInstallation(installed.getEdition().edition(), installed.getResolvedVersion(), request.getProcessContext(),
11✔
394
        request.isAdditionalInstallation());
1✔
395
  }
396

397
  /**
398
   * @param edition the {@link #getConfiguredEdition() edition}.
399
   * @param installedVersion the {@link #getConfiguredVersion() version}.
400
   * @param environmentContext the {@link EnvironmentContext}.
401
   * @param extraInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
402
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
403
   * @return the {@link ToolInstallation}.
404
   */
405
  protected ToolInstallation createExistingToolInstallation(String edition, VersionIdentifier installedVersion, EnvironmentContext environmentContext,
406
      boolean extraInstallation) {
407

408
    Path installationPath = getInstallationPath(edition, installedVersion);
5✔
409
    return createToolInstallation(installationPath, installedVersion, false, environmentContext, extraInstallation);
8✔
410
  }
411

412
  /**
413
   * @param rootDir the {@link ToolInstallation#rootDir() top-level installation directory}.
414
   * @param version the installed {@link VersionIdentifier}.
415
   * @param newInstallation {@link ToolInstallation#newInstallation() new installation} flag.
416
   * @param environmentContext the {@link EnvironmentContext}.
417
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
418
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
419
   * @return the {@link ToolInstallation}.
420
   */
421
  protected ToolInstallation createToolInstallation(Path rootDir, VersionIdentifier version, boolean newInstallation,
422
      EnvironmentContext environmentContext, boolean additionalInstallation) {
423

424
    Path linkDir = rootDir;
2✔
425
    Path binDir = rootDir;
2✔
426
    if (rootDir != null) {
2✔
427
      // on MacOS applications have a very strange structure - see JavaDoc of findLinkDir and ToolInstallation.linkDir for details.
428
      linkDir = getMacOsHelper().findLinkDir(rootDir, getBinaryName());
7✔
429
      binDir = this.context.getFileAccess().getBinPath(linkDir);
6✔
430
    }
431
    return createToolInstallation(rootDir, linkDir, binDir, version, newInstallation, environmentContext, additionalInstallation);
10✔
432
  }
433

434
  /**
435
   * @param rootDir the {@link ToolInstallation#rootDir() top-level installation directory}.
436
   * @param linkDir the {@link ToolInstallation#linkDir() link directory}.
437
   * @param binDir the {@link ToolInstallation#binDir() bin directory}.
438
   * @param version the installed {@link VersionIdentifier}.
439
   * @param newInstallation {@link ToolInstallation#newInstallation() new installation} flag.
440
   * @param environmentContext the {@link EnvironmentContext}.
441
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
442
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
443
   * @return the {@link ToolInstallation}.
444
   */
445
  protected ToolInstallation createToolInstallation(Path rootDir, Path linkDir, Path binDir, VersionIdentifier version, boolean newInstallation,
446
      EnvironmentContext environmentContext, boolean additionalInstallation) {
447

448
    if (linkDir != rootDir) {
3✔
449
      assert (!linkDir.equals(rootDir));
5!
450
      Path toolVersionFile = rootDir.resolve(IdeContext.FILE_SOFTWARE_VERSION);
4✔
451
      if (Files.exists(toolVersionFile)) {
5!
452
        this.context.getFileAccess().copy(toolVersionFile, linkDir, FileCopyMode.COPY_FILE_OVERRIDE);
7✔
453
      }
454
    }
455
    ToolInstallation toolInstallation = new ToolInstallation(rootDir, linkDir, binDir, version, newInstallation);
9✔
456
    setEnvironment(environmentContext, toolInstallation, additionalInstallation);
5✔
457
    return toolInstallation;
2✔
458
  }
459

460
  /**
461
   * Called if the tool {@link ToolInstallRequest#isAlreadyInstalled() is already installed in the correct edition and version} so we can skip the
462
   * installation.
463
   *
464
   * @param request the {@link ToolInstallRequest}.
465
   * @return the {@link ToolInstallation}.
466
   */
467
  protected ToolInstallation toolAlreadyInstalled(ToolInstallRequest request) {
468

469
    logToolAlreadyInstalled(request);
3✔
470
    cveCheck(request);
4✔
471
    postInstall(request);
3✔
472
    return createExistingToolInstallation(request);
4✔
473
  }
474

475
  /**
476
   * Log that the tool is already installed.
477
   *
478
   * @param request the {@link ToolInstallRequest}.
479
   */
480
  protected void logToolAlreadyInstalled(ToolInstallRequest request) {
481
    IdeSubLogger logger;
482
    if (request.isSilent()) {
3✔
483
      logger = this.context.debug();
5✔
484
    } else {
485
      logger = this.context.info();
4✔
486
    }
487
    ToolEditionAndVersion installed = request.getInstalled();
3✔
488
    logger.log("Version {} of tool {} is already installed", installed.getVersion(), installed.getEdition());
16✔
489
  }
1✔
490

491
  /**
492
   * Method to get the home path of the given {@link ToolInstallation}.
493
   *
494
   * @param toolInstallation the {@link ToolInstallation}.
495
   * @return the Path to the home of the tool
496
   */
497
  protected Path getToolHomePath(ToolInstallation toolInstallation) {
498
    return toolInstallation.linkDir();
3✔
499
  }
500

501
  /**
502
   * Method to set environment variables for the process context.
503
   *
504
   * @param environmentContext the {@link EnvironmentContext} where to {@link EnvironmentContext#withEnvVar(String, String) set environment variables} for
505
   *     this tool.
506
   * @param toolInstallation the {@link ToolInstallation}.
507
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
508
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
509
   */
510
  public void setEnvironment(EnvironmentContext environmentContext, ToolInstallation toolInstallation, boolean additionalInstallation) {
511

512
    String pathVariable = EnvironmentVariables.getToolVariablePrefix(this.tool) + "_HOME";
5✔
513
    Path toolHomePath = getToolHomePath(toolInstallation);
4✔
514
    if (toolHomePath != null) {
2✔
515
      environmentContext.withEnvVar(pathVariable, toolHomePath.toString());
6✔
516
    }
517
    if (additionalInstallation) {
2✔
518
      environmentContext.withPathEntry(toolInstallation.binDir());
5✔
519
    }
520
  }
1✔
521

522
  /**
523
   * @return {@code true} to extract (unpack) the downloaded binary file, {@code false} otherwise.
524
   */
525
  protected boolean isExtract() {
526

527
    return true;
2✔
528
  }
529

530
  /**
531
   * 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
532
   * something better, we will suggest this to the user and ask him to make his choice.
533
   *
534
   * @param request the {@link ToolInstallRequest}.
535
   * @return the {@link VersionIdentifier} to install. The will may be asked (unless {@code skipSuggestions} is {@code true}) and might choose a different
536
   *     version than the originally requested one.
537
   */
538
  protected VersionIdentifier cveCheck(ToolInstallRequest request) {
539

540
    ToolEditionAndVersion requested = request.getRequested();
3✔
541
    VersionIdentifier resolvedVersion = requested.getResolvedVersion();
3✔
542
    if (request.isCveCheckDone()) {
3✔
543
      return resolvedVersion;
2✔
544
    }
545
    ToolEdition toolEdition = requested.getEdition();
3✔
546
    GenericVersionRange allowedVersions = requested.getVersion();
3✔
547
    boolean requireStableVersion = true;
2✔
548
    if (allowedVersions instanceof VersionIdentifier vi) {
6✔
549
      requireStableVersion = vi.isStable();
3✔
550
    }
551
    ToolSecurity toolSecurity = this.context.getDefaultToolRepository().findSecurity(this.tool, toolEdition.edition());
9✔
552
    double minSeverity = IdeVariables.CVE_MIN_SEVERITY.get(context);
7✔
553
    ToolVulnerabilities currentVulnerabilities = toolSecurity.findCves(resolvedVersion, this.context, minSeverity);
7✔
554
    ToolVersionChoice currentChoice = ToolVersionChoice.ofCurrent(requested, currentVulnerabilities);
4✔
555
    request.setCveCheckDone();
2✔
556
    if (currentChoice.logAndCheckIfEmpty(this.context)) {
5✔
557
      return resolvedVersion;
2✔
558
    }
559
    boolean alreadyInstalled = request.isAlreadyInstalled();
3✔
560
    boolean directForceInstall = this.context.isForceMode() && request.isDirect();
6!
561
    if (alreadyInstalled && !directForceInstall) {
2!
562
      // currently for a transitive dependency it does not make sense to suggest alternative versions, since the choice is not stored anywhere,
563
      // 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
564
      // (e.g. upgrade the tool with the dependency that is causing this).
565
      this.context.interaction("Please run 'ide -f install {}' to check for update suggestions!", this.tool);
×
566
      return resolvedVersion;
×
567
    }
568
    ToolVersionChoice latest = null;
2✔
569
    ToolVulnerabilities latestVulnerabilities = currentVulnerabilities;
2✔
570
    ToolVersionChoice nearest = null;
2✔
571
    ToolVulnerabilities nearestVulnerabilities = currentVulnerabilities;
2✔
572
    List<VersionIdentifier> toolVersions = getVersions();
3✔
573
    for (VersionIdentifier version : toolVersions) {
10✔
574
      if (acceptVersion(version, allowedVersions, requireStableVersion)) {
5!
575
        ToolVulnerabilities newVulnerabilities = toolSecurity.findCves(version, this.context, minSeverity);
7✔
576
        if (newVulnerabilities.isSafer(latestVulnerabilities)) {
4✔
577
          // we found a better/safer version
578
          ToolEditionAndVersion toolEditionAndVersion = new ToolEditionAndVersion(toolEdition, version);
6✔
579
          if (version.isGreater(resolvedVersion)) {
4!
580
            latestVulnerabilities = newVulnerabilities;
2✔
581
            latest = ToolVersionChoice.ofLatest(toolEditionAndVersion, latestVulnerabilities);
4✔
582
            nearest = null;
3✔
583
          } else {
584
            nearestVulnerabilities = newVulnerabilities;
×
585
            nearest = ToolVersionChoice.ofNearest(toolEditionAndVersion, nearestVulnerabilities);
×
586
          }
587
        } else if (newVulnerabilities.isSaferOrEqual(nearestVulnerabilities)) {
5✔
588
          if (newVulnerabilities.isSafer(nearestVulnerabilities) || version.isGreater(resolvedVersion)) {
8✔
589
            nearest = ToolVersionChoice.ofNearest(new ToolEditionAndVersion(toolEdition, version), newVulnerabilities);
8✔
590
          }
591
          nearestVulnerabilities = newVulnerabilities;
2✔
592
        }
593
      }
594
    }
1✔
595
    if ((latest == null) && (nearest == null)) {
2!
596
      this.context.warning(
×
597
          "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.");
598
      if (alreadyInstalled) {
×
599
        // we came here via "ide -f install ..." but no alternative is available
600
        return resolvedVersion;
×
601
      }
602
    }
603
    List<ToolVersionChoice> choices = new ArrayList<>();
4✔
604
    choices.add(currentChoice);
4✔
605
    boolean addSuggestions;
606
    if (this.context.isForceMode() && request.isDirect()) {
4!
607
      addSuggestions = true;
×
608
    } else {
609
      List<String> skipCveFixTools = IdeVariables.SKIP_CVE_FIX.get(this.context);
6✔
610
      addSuggestions = !skipCveFixTools.contains(this.tool);
8!
611
    }
612
    if (nearest != null) {
2!
613
      if (addSuggestions) {
2!
614
        choices.add(nearest);
4✔
615
      }
616
      nearest.logAndCheckIfEmpty(this.context);
5✔
617
    }
618
    if (latest != null) {
2!
619
      if (addSuggestions) {
2!
620
        choices.add(latest);
4✔
621
      }
622
      latest.logAndCheckIfEmpty(this.context);
5✔
623
    }
624
    ToolVersionChoice[] choicesArray = choices.toArray(ToolVersionChoice[]::new);
8✔
625
    this.context.warning(
4✔
626
        "Please note that by selecting an unsafe version to install, you accept the risk to be attacked.");
627
    ToolVersionChoice answer = this.context.question(choicesArray, "Which version do you want to install?");
9✔
628
    VersionIdentifier version = answer.toolEditionAndVersion().getResolvedVersion();
4✔
629
    requested.setResolvedVersion(version);
3✔
630
    return version;
2✔
631
  }
632

633
  private static boolean acceptVersion(VersionIdentifier version, GenericVersionRange allowedVersions, boolean requireStableVersion) {
634
    if (allowedVersions.isPattern() && !allowedVersions.contains(version)) {
3!
635
      return false;
×
636
    } else if (requireStableVersion && !version.isStable()) {
5!
637
      return false;
×
638
    }
639
    return true;
2✔
640
  }
641

642
  /**
643
   * @return the {@link MacOsHelper} instance.
644
   */
645
  protected MacOsHelper getMacOsHelper() {
646

647
    if (this.macOsHelper == null) {
3✔
648
      this.macOsHelper = new MacOsHelper(this.context);
7✔
649
    }
650
    return this.macOsHelper;
3✔
651
  }
652

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

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

663
    return getInstalledVersion() != null;
7✔
664
  }
665

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

671
  /**
672
   * Uninstalls the {@link #getName() tool}.
673
   */
674
  public abstract void uninstall();
675

676
  /**
677
   * @return the {@link ToolRepository}.
678
   */
679
  public ToolRepository getToolRepository() {
680

681
    return this.context.getDefaultToolRepository();
4✔
682
  }
683

684
  /**
685
   * List the available editions of this tool.
686
   */
687
  public void listEditions() {
688

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

695
  /**
696
   * List the available versions of this tool.
697
   */
698
  public void listVersions() {
699

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

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

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

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

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

739
    setVersion(version, hint, null);
5✔
740
  }
1✔
741

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

751
    String edition = getConfiguredEdition();
3✔
752
    ToolRepository toolRepository = getToolRepository();
3✔
753

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

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

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

783
    setEdition(edition, true);
4✔
784
  }
1✔
785

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

794
    setEdition(edition, hint, null);
5✔
795
  }
1✔
796

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

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

810
    if (destination == null) {
2✔
811
      //use default location
812
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
813
    }
814

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

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

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

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

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

856
    return null;
×
857
  }
858

859
  /**
860
   * Creates a start script for the tool using the tool name.
861
   *
862
   * @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
863
   *     instead.
864
   * @param binary name of the binary to execute from the start script.
865
   */
866
  protected void createStartScript(Path targetDir, String binary) {
867

868
    createStartScript(targetDir, binary, false);
×
869
  }
×
870

871
  /**
872
   * Creates a start script for the tool using the tool name.
873
   *
874
   * @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
875
   *     instead.
876
   * @param binary name of the binary to execute from the start script.
877
   * @param background {@code true} to run the {@code binary} in background, {@code false} otherwise (foreground).
878
   */
879
  protected void createStartScript(Path targetDir, String binary, boolean background) {
880

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

907
  @Override
908
  public void reset() {
909
    super.reset();
2✔
910
    this.executionDirectory = null;
3✔
911
  }
1✔
912

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

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

926

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

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