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

devonfw / IDEasy / 20261670198

16 Dec 2025 08:40AM UTC coverage: 70.142% (+0.004%) from 70.138%
20261670198

push

github

web-flow
#1645: Hotfix for the NullPointerException (#1652)

Co-authored-by: Jörg Hohwiller <hohwille@users.noreply.github.com>

3973 of 6231 branches covered (63.76%)

Branch coverage included in aggregate %.

10167 of 13928 relevant lines covered (73.0%)

3.15 hits per line

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

75.97
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.asList());
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, List)
152
   */
153
  public ProcessResult runTool(List<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, List<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, List<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, List<String> args) {
202

203
    if (request.isCveCheckDone()) {
3!
204
      // if the CVE check has already been done, we can assume that the install(request) has already been called before
205
      // most likely a postInstall* method was overridden calling this method with the same request what is a programming error
206
      // we render this warning so the error gets detected and can be fixed but we do not block the user by skipping the installation.
207
      this.context.warning().log(new RuntimeException(), "Preventing infinity loop during installation of {}", request.getRequested());
×
208
    } else {
209
      install(request);
4✔
210
    }
211
    return runTool(request.getProcessContext(), processMode, args);
7✔
212
  }
213

214
  /**
215
   * @param pc the {@link ProcessContext}.
216
   * @param processMode the {@link ProcessMode}. Should typically be {@link ProcessMode#DEFAULT} or {@link ProcessMode#BACKGROUND}.
217
   * @param args the command-line arguments to run the tool.
218
   * @return the {@link ProcessResult result}.
219
   */
220
  public ProcessResult runTool(ProcessContext pc, ProcessMode processMode, List<String> args) {
221

222
    if (this.executionDirectory != null) {
3!
223
      pc.directory(this.executionDirectory);
×
224
    }
225
    configureToolBinary(pc, processMode);
4✔
226
    configureToolArgs(pc, processMode, args);
5✔
227
    return pc.run(processMode);
4✔
228
  }
229

230
  /**
231
   * @param pc the {@link ProcessContext}.
232
   * @param processMode the {@link ProcessMode}.
233
   */
234
  protected void configureToolBinary(ProcessContext pc, ProcessMode processMode) {
235

236
    pc.executable(Path.of(getBinaryName()));
8✔
237
  }
1✔
238

239
  /**
240
   * @param pc the {@link ProcessContext}.
241
   * @param processMode the {@link ProcessMode}.
242
   * @param args the command-line arguments to {@link ProcessContext#addArgs(List) add}.
243
   */
244
  protected void configureToolArgs(ProcessContext pc, ProcessMode processMode, List<String> args) {
245

246
    pc.addArgs(args);
4✔
247
  }
1✔
248

249
  /**
250
   * Installs or updates the managed {@link #getName() tool}.
251
   *
252
   * @return the {@link ToolInstallation}.
253
   */
254
  public ToolInstallation install() {
255

256
    return install(true);
4✔
257
  }
258

259
  /**
260
   * Performs the installation of the {@link #getName() tool} managed by this {@link com.devonfw.tools.ide.commandlet.Commandlet}.
261
   *
262
   * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
263
   * @return the {@link ToolInstallation}.
264
   */
265
  public ToolInstallation install(boolean silent) {
266
    return install(new ToolInstallRequest(silent));
7✔
267
  }
268

269
  /**
270
   * Performs the installation (install, update, downgrade) of the {@link #getName() tool} managed by this {@link ToolCommandlet}.
271
   *
272
   * @param request the {@link ToolInstallRequest}.
273
   * @return the {@link ToolInstallation}.
274
   */
275
  public ToolInstallation install(ToolInstallRequest request) {
276

277
    completeRequest(request);
3✔
278
    if (request.isInstallLoop(this.context)) {
5!
279
      return toolAlreadyInstalled(request);
×
280
    }
281
    return doInstall(request);
4✔
282
  }
283

284
  /**
285
   * Performs the installation (install, update, downgrade) of the {@link #getName() tool} managed by this {@link ToolCommandlet}.
286
   *
287
   * @param request the {@link ToolInstallRequest}.
288
   * @return the {@link ToolInstallation}.
289
   */
290
  protected abstract ToolInstallation doInstall(ToolInstallRequest request);
291

292
  /**
293
   * @param request the {@link ToolInstallRequest} to complete (fill values that are currently {@code null}).
294
   */
295
  protected void completeRequest(ToolInstallRequest request) {
296

297
    completeRequestInstalled(request);
3✔
298
    completeRequestRequested(request); // depends on completeRequestInstalled
3✔
299
    completeRequestProcessContext(request);
3✔
300
  }
1✔
301

302
  private void completeRequestProcessContext(ToolInstallRequest request) {
303
    if (request.getProcessContext() == null) {
3✔
304
      ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_CLI);
6✔
305
      request.setProcessContext(pc);
3✔
306
    }
307
  }
1✔
308

309
  private void completeRequestInstalled(ToolInstallRequest request) {
310

311
    ToolEditionAndVersion installedToolVersion = request.getInstalled();
3✔
312
    if (installedToolVersion == null) {
2✔
313
      installedToolVersion = new ToolEditionAndVersion((GenericVersionRange) null);
6✔
314
      request.setInstalled(installedToolVersion);
3✔
315
    }
316
    if (installedToolVersion.getVersion() == null) {
3✔
317
      VersionIdentifier installedVersion = getInstalledVersion();
3✔
318
      if (installedVersion == null) {
2✔
319
        return;
1✔
320
      }
321
      installedToolVersion.setVersion(installedVersion);
3✔
322
    }
323
    if (installedToolVersion.getEdition() == null) {
3✔
324
      installedToolVersion.setEdition(new ToolEdition(this.tool, getInstalledEdition()));
9✔
325
    }
326
    assert installedToolVersion.getResolvedVersion() != null;
4!
327
  }
1✔
328

329
  private void completeRequestRequested(ToolInstallRequest request) {
330

331
    ToolEdition edition;
332
    ToolEditionAndVersion requested = request.getRequested();
3✔
333
    if (requested == null) {
2✔
334
      edition = new ToolEdition(this.tool, getConfiguredEdition());
8✔
335
      requested = new ToolEditionAndVersion(edition);
5✔
336
      request.setRequested(requested);
4✔
337
    } else {
338
      edition = requested.getEdition();
3✔
339
      if (edition == null) {
2✔
340
        edition = new ToolEdition(this.tool, getConfiguredEdition());
8✔
341
        requested.setEdition(edition);
3✔
342
      }
343
    }
344
    GenericVersionRange version = requested.getVersion();
3✔
345
    if (version == null) {
2✔
346
      version = getConfiguredVersion();
3✔
347
      requested.setVersion(version);
3✔
348
    }
349
    VersionIdentifier resolvedVersion = requested.getResolvedVersion();
3✔
350
    if (resolvedVersion == null) {
2✔
351
      if (this.context.isSkipUpdatesMode()) {
4✔
352
        ToolEditionAndVersion installed = request.getInstalled();
3✔
353
        if (installed != null) {
2!
354
          VersionIdentifier installedVersion = installed.getResolvedVersion();
3✔
355
          if (version.contains(installedVersion)) {
4✔
356
            resolvedVersion = installedVersion;
2✔
357
          }
358
        }
359
      }
360
      if (resolvedVersion == null) {
2✔
361
        resolvedVersion = getToolRepository().resolveVersion(this.tool, edition.edition(), version, this);
10✔
362
      }
363
      requested.setResolvedVersion(resolvedVersion);
3✔
364
    }
365
  }
1✔
366

367
  /**
368
   * This method is called after a tool was requested to be installed or updated.
369
   *
370
   * @param request {@code true} the {@link ToolInstallRequest}.
371
   */
372
  protected void postInstall(ToolInstallRequest request) {
373

374
    if (!request.isAlreadyInstalled()) {
3✔
375
      postInstallOnNewInstallation(request);
3✔
376
    }
377
  }
1✔
378

379
  /**
380
   * This method is called after a tool was requested to be installed or updated and a new installation was performed.
381
   *
382
   * @param request {@code true} the {@link ToolInstallRequest}.
383
   */
384
  protected void postInstallOnNewInstallation(ToolInstallRequest request) {
385

386
    // nothing to do by default
387
  }
1✔
388

389
  /**
390
   * @param edition the {@link #getInstalledEdition() edition}.
391
   * @param version the {@link #getInstalledVersion() version}.
392
   * @return the {@link Path} where this tool is installed (physically) or {@code null} if not available.
393
   */
394
  protected abstract Path getInstallationPath(String edition, VersionIdentifier version);
395

396
  /**
397
   * @param request the {@link ToolInstallRequest}.
398
   * @return the existing {@link ToolInstallation}.
399
   */
400
  protected ToolInstallation createExistingToolInstallation(ToolInstallRequest request) {
401

402
    ToolEditionAndVersion installed = request.getInstalled();
3✔
403

404
    String edition = this.tool;
3✔
405
    VersionIdentifier resolvedVersion = VersionIdentifier.LATEST;
2✔
406

407
    if (installed != null) {
2!
408
      if (installed.getEdition() != null) {
3!
409
        edition = installed.getEdition().edition();
4✔
410
      }
411
      if (installed.getResolvedVersion() != null) {
3!
412
        resolvedVersion = installed.getResolvedVersion();
3✔
413
      }
414
    }
415

416
    return createExistingToolInstallation(edition, resolvedVersion, request.getProcessContext(),
8✔
417
        request.isAdditionalInstallation());
1✔
418
  }
419

420
  /**
421
   * @param edition the {@link #getConfiguredEdition() edition}.
422
   * @param installedVersion the {@link #getConfiguredVersion() version}.
423
   * @param environmentContext the {@link EnvironmentContext}.
424
   * @param extraInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
425
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
426
   * @return the {@link ToolInstallation}.
427
   */
428
  protected ToolInstallation createExistingToolInstallation(String edition, VersionIdentifier installedVersion, EnvironmentContext environmentContext,
429
      boolean extraInstallation) {
430

431
    Path installationPath = getInstallationPath(edition, installedVersion);
5✔
432
    return createToolInstallation(installationPath, installedVersion, false, environmentContext, extraInstallation);
8✔
433
  }
434

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

447
    Path linkDir = rootDir;
2✔
448
    Path binDir = rootDir;
2✔
449
    if (rootDir != null) {
2✔
450
      // on MacOS applications have a very strange structure - see JavaDoc of findLinkDir and ToolInstallation.linkDir for details.
451
      linkDir = getMacOsHelper().findLinkDir(rootDir, getBinaryName());
7✔
452
      binDir = this.context.getFileAccess().getBinPath(linkDir);
6✔
453
    }
454
    return createToolInstallation(rootDir, linkDir, binDir, version, newInstallation, environmentContext, additionalInstallation);
10✔
455
  }
456

457
  /**
458
   * @param rootDir the {@link ToolInstallation#rootDir() top-level installation directory}.
459
   * @param linkDir the {@link ToolInstallation#linkDir() link directory}.
460
   * @param binDir the {@link ToolInstallation#binDir() bin directory}.
461
   * @param version the installed {@link VersionIdentifier}.
462
   * @param newInstallation {@link ToolInstallation#newInstallation() new installation} flag.
463
   * @param environmentContext the {@link EnvironmentContext}.
464
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
465
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
466
   * @return the {@link ToolInstallation}.
467
   */
468
  protected ToolInstallation createToolInstallation(Path rootDir, Path linkDir, Path binDir, VersionIdentifier version, boolean newInstallation,
469
      EnvironmentContext environmentContext, boolean additionalInstallation) {
470

471
    if (linkDir != rootDir) {
3✔
472
      assert (!linkDir.equals(rootDir));
5!
473
      Path toolVersionFile = rootDir.resolve(IdeContext.FILE_SOFTWARE_VERSION);
4✔
474
      if (Files.exists(toolVersionFile)) {
5!
475
        this.context.getFileAccess().copy(toolVersionFile, linkDir, FileCopyMode.COPY_FILE_OVERRIDE);
7✔
476
      }
477
    }
478
    ToolInstallation toolInstallation = new ToolInstallation(rootDir, linkDir, binDir, version, newInstallation);
9✔
479
    setEnvironment(environmentContext, toolInstallation, additionalInstallation);
5✔
480
    return toolInstallation;
2✔
481
  }
482

483
  /**
484
   * Called if the tool {@link ToolInstallRequest#isAlreadyInstalled() is already installed in the correct edition and version} so we can skip the
485
   * installation.
486
   *
487
   * @param request the {@link ToolInstallRequest}.
488
   * @return the {@link ToolInstallation}.
489
   */
490
  protected ToolInstallation toolAlreadyInstalled(ToolInstallRequest request) {
491

492
    logToolAlreadyInstalled(request);
3✔
493
    cveCheck(request);
4✔
494
    postInstall(request);
3✔
495
    return createExistingToolInstallation(request);
4✔
496
  }
497

498
  /**
499
   * Log that the tool is already installed.
500
   *
501
   * @param request the {@link ToolInstallRequest}.
502
   */
503
  protected void logToolAlreadyInstalled(ToolInstallRequest request) {
504
    IdeSubLogger logger;
505
    if (request.isSilent()) {
3✔
506
      logger = this.context.debug();
5✔
507
    } else {
508
      logger = this.context.info();
4✔
509
    }
510
    ToolEditionAndVersion installed = request.getInstalled();
3✔
511
    logger.log("Version {} of tool {} is already installed", installed.getVersion(), installed.getEdition());
16✔
512
  }
1✔
513

514
  /**
515
   * Method to get the home path of the given {@link ToolInstallation}.
516
   *
517
   * @param toolInstallation the {@link ToolInstallation}.
518
   * @return the Path to the home of the tool
519
   */
520
  protected Path getToolHomePath(ToolInstallation toolInstallation) {
521
    return toolInstallation.linkDir();
3✔
522
  }
523

524
  /**
525
   * Method to set environment variables for the process context.
526
   *
527
   * @param environmentContext the {@link EnvironmentContext} where to {@link EnvironmentContext#withEnvVar(String, String) set environment variables} for
528
   *     this tool.
529
   * @param toolInstallation the {@link ToolInstallation}.
530
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
531
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
532
   */
533
  public void setEnvironment(EnvironmentContext environmentContext, ToolInstallation toolInstallation, boolean additionalInstallation) {
534

535
    String pathVariable = EnvironmentVariables.getToolVariablePrefix(this.tool) + "_HOME";
5✔
536
    Path toolHomePath = getToolHomePath(toolInstallation);
4✔
537
    if (toolHomePath != null) {
2✔
538
      environmentContext.withEnvVar(pathVariable, toolHomePath.toString());
6✔
539
    }
540
    if (additionalInstallation) {
2✔
541
      environmentContext.withPathEntry(toolInstallation.binDir());
5✔
542
    }
543
  }
1✔
544

545
  /**
546
   * @return {@code true} to extract (unpack) the downloaded binary file, {@code false} otherwise.
547
   */
548
  protected boolean isExtract() {
549

550
    return true;
2✔
551
  }
552

553
  /**
554
   * 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
555
   * something better, we will suggest this to the user and ask him to make his choice.
556
   *
557
   * @param request the {@link ToolInstallRequest}.
558
   * @return the {@link VersionIdentifier} to install. The will may be asked (unless {@code skipSuggestions} is {@code true}) and might choose a different
559
   *     version than the originally requested one.
560
   */
561
  protected VersionIdentifier cveCheck(ToolInstallRequest request) {
562

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

656
  private static boolean acceptVersion(VersionIdentifier version, GenericVersionRange allowedVersions, boolean requireStableVersion) {
657
    if (allowedVersions.isPattern() && !allowedVersions.contains(version)) {
3!
658
      return false;
×
659
    } else if (requireStableVersion && !version.isStable()) {
5!
660
      return false;
×
661
    }
662
    return true;
2✔
663
  }
664

665
  /**
666
   * @return the {@link MacOsHelper} instance.
667
   */
668
  protected MacOsHelper getMacOsHelper() {
669

670
    if (this.macOsHelper == null) {
3✔
671
      this.macOsHelper = new MacOsHelper(this.context);
7✔
672
    }
673
    return this.macOsHelper;
3✔
674
  }
675

676
  /**
677
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
678
   */
679
  public abstract VersionIdentifier getInstalledVersion();
680

681
  /**
682
   * @return {@code true} if this tool is installed, {@code false} otherwise.
683
   */
684
  public boolean isInstalled() {
685

686
    return getInstalledVersion() != null;
7✔
687
  }
688

689
  /**
690
   * @return the installed edition of this tool or {@code null} if not installed.
691
   */
692
  public abstract String getInstalledEdition();
693

694
  /**
695
   * Uninstalls the {@link #getName() tool}.
696
   */
697
  public abstract void uninstall();
698

699
  /**
700
   * @return the {@link ToolRepository}.
701
   */
702
  public ToolRepository getToolRepository() {
703

704
    return this.context.getDefaultToolRepository();
4✔
705
  }
706

707
  /**
708
   * List the available editions of this tool.
709
   */
710
  public void listEditions() {
711

712
    List<String> editions = getToolRepository().getSortedEditions(getName());
6✔
713
    for (String edition : editions) {
10✔
714
      this.context.info(edition);
4✔
715
    }
1✔
716
  }
1✔
717

718
  /**
719
   * List the available versions of this tool.
720
   */
721
  public void listVersions() {
722

723
    List<VersionIdentifier> versions = getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
724
    for (VersionIdentifier vi : versions) {
10✔
725
      this.context.info(vi.toString());
5✔
726
    }
1✔
727
  }
1✔
728

729
  /**
730
   * @return the {@link com.devonfw.tools.ide.tool.repository.DefaultToolRepository#getSortedVersions(String, String, ToolCommandlet) sorted versions} of this
731
   *     tool.
732
   */
733
  public List<VersionIdentifier> getVersions() {
734
    return getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
735
  }
736

737
  /**
738
   * Sets the tool version in the environment variable configuration file.
739
   *
740
   * @param version the version (pattern) to set.
741
   */
742
  public void setVersion(String version) {
743

744
    if ((version == null) || version.isBlank()) {
×
745
      throw new IllegalStateException("Version has to be specified!");
×
746
    }
747
    VersionIdentifier configuredVersion = VersionIdentifier.of(version);
×
748
    if (!configuredVersion.isPattern() && !configuredVersion.isValid()) {
×
749
      this.context.warning("Version {} seems to be invalid", version);
×
750
    }
751
    setVersion(configuredVersion, true);
×
752
  }
×
753

754
  /**
755
   * Sets the tool version in the environment variable configuration file.
756
   *
757
   * @param version the version to set. May also be a {@link VersionIdentifier#isPattern() version pattern}.
758
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
759
   */
760
  public void setVersion(VersionIdentifier version, boolean hint) {
761

762
    setVersion(version, hint, null);
5✔
763
  }
1✔
764

765
  /**
766
   * Sets the tool version in the environment variable configuration file.
767
   *
768
   * @param version the version to set. May also be a {@link VersionIdentifier#isPattern() version pattern}.
769
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
770
   * @param destination - the destination for the property to be set
771
   */
772
  public void setVersion(VersionIdentifier version, boolean hint, EnvironmentVariablesFiles destination) {
773

774
    String edition = getConfiguredEdition();
3✔
775
    ToolRepository toolRepository = getToolRepository();
3✔
776

777
    EnvironmentVariables variables = this.context.getVariables();
4✔
778
    if (destination == null) {
2✔
779
      //use default location
780
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
781
    }
782
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
783
    String name = EnvironmentVariables.getToolVersionVariable(this.tool);
4✔
784

785
    toolRepository.resolveVersion(this.tool, edition, version, this); // verify that the version actually exists
8✔
786
    settingsVariables.set(name, version.toString(), false);
7✔
787
    settingsVariables.save();
2✔
788
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
789
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
790
      this.context.warning("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name,
13✔
791
          declaringVariables.getSource());
2✔
792
    }
793
    if (hint) {
2✔
794
      this.context.info("To install that version call the following command:");
4✔
795
      this.context.info("ide install {}", this.tool);
11✔
796
    }
797
  }
1✔
798

799
  /**
800
   * Sets the tool edition in the environment variable configuration file.
801
   *
802
   * @param edition the edition to set.
803
   */
804
  public void setEdition(String edition) {
805

806
    setEdition(edition, true);
4✔
807
  }
1✔
808

809
  /**
810
   * Sets the tool edition in the environment variable configuration file.
811
   *
812
   * @param edition the edition to set
813
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
814
   */
815
  public void setEdition(String edition, boolean hint) {
816

817
    setEdition(edition, hint, null);
5✔
818
  }
1✔
819

820
  /**
821
   * Sets the tool edition in the environment variable configuration file.
822
   *
823
   * @param edition the edition to set
824
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
825
   * @param destination - the destination for the property to be set
826
   */
827
  public void setEdition(String edition, boolean hint, EnvironmentVariablesFiles destination) {
828

829
    if ((edition == null) || edition.isBlank()) {
5!
830
      throw new IllegalStateException("Edition has to be specified!");
×
831
    }
832

833
    if (destination == null) {
2✔
834
      //use default location
835
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
836
    }
837

838
    if (!getToolRepository().getSortedEditions(this.tool).contains(edition)) {
8✔
839
      this.context.warning("Edition {} seems to be invalid", edition);
10✔
840
    }
841
    EnvironmentVariables variables = this.context.getVariables();
4✔
842
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
843
    String name = EnvironmentVariables.getToolEditionVariable(this.tool);
4✔
844
    settingsVariables.set(name, edition, false);
6✔
845
    settingsVariables.save();
2✔
846

847
    this.context.info("{}={} has been set in {}", name, edition, settingsVariables.getSource());
19✔
848
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
849
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
850
      this.context.warning("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name,
13✔
851
          declaringVariables.getSource());
2✔
852
    }
853
    if (hint) {
2!
854
      this.context.info("To install that edition call the following command:");
4✔
855
      this.context.info("ide install {}", this.tool);
11✔
856
    }
857
  }
1✔
858

859
  /**
860
   * Runs the tool's help command to provide the user with usage information.
861
   */
862
  @Override
863
  public void printHelp(NlsBundle bundle) {
864

865
    super.printHelp(bundle);
3✔
866
    String toolHelpArgs = getToolHelpArguments();
3✔
867
    if (toolHelpArgs != null && getInstalledVersion() != null) {
5!
868
      ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.LOG_WARNING)
6✔
869
          .executable(Path.of(getBinaryName())).addArgs(toolHelpArgs);
13✔
870
      pc.run(ProcessMode.DEFAULT);
4✔
871
    }
872
  }
1✔
873

874
  /**
875
   * @return the tool's specific help command. Usually help, --help or -h. Return null if not applicable.
876
   */
877
  public String getToolHelpArguments() {
878

879
    return null;
×
880
  }
881

882
  /**
883
   * Creates a start script for the tool using the tool name.
884
   *
885
   * @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
886
   *     instead.
887
   * @param binary name of the binary to execute from the start script.
888
   */
889
  protected void createStartScript(Path targetDir, String binary) {
890

891
    createStartScript(targetDir, binary, false);
×
892
  }
×
893

894
  /**
895
   * Creates a start script for the tool using the tool name.
896
   *
897
   * @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
898
   *     instead.
899
   * @param binary name of the binary to execute from the start script.
900
   * @param background {@code true} to run the {@code binary} in background, {@code false} otherwise (foreground).
901
   */
902
  protected void createStartScript(Path targetDir, String binary, boolean background) {
903

904
    Path binFolder = targetDir.resolve("bin");
×
905
    if (!Files.exists(binFolder)) {
×
906
      if (this.context.getSystemInfo().isMac()) {
×
907
        MacOsHelper macOsHelper = getMacOsHelper();
×
908
        Path appDir = macOsHelper.findAppDir(targetDir);
×
909
        binFolder = macOsHelper.findLinkDir(appDir, binary);
×
910
      } else {
×
911
        binFolder = targetDir;
×
912
      }
913
      assert (Files.exists(binFolder));
×
914
    }
915
    Path bashFile = binFolder.resolve(getName());
×
916
    String bashFileContentStart = "#!/usr/bin/env bash\n\"$(dirname \"$0\")/";
×
917
    String bashFileContentEnd = "\" $@";
×
918
    if (background) {
×
919
      bashFileContentEnd += " &";
×
920
    }
921
    try {
922
      Files.writeString(bashFile, bashFileContentStart + binary + bashFileContentEnd);
×
923
    } catch (IOException e) {
×
924
      throw new RuntimeException(e);
×
925
    }
×
926
    assert (Files.exists(bashFile));
×
927
    context.getFileAccess().makeExecutable(bashFile);
×
928
  }
×
929

930
  @Override
931
  public void reset() {
932
    super.reset();
2✔
933
    this.executionDirectory = null;
3✔
934
  }
1✔
935

936
  /**
937
   * @param step the {@link Step} to get {@link Step#asSuccess() success logger} from. May be {@code null}.
938
   * @return the {@link IdeSubLogger} from {@link Step#asSuccess()} or {@link IdeContext#success()} as fallback.
939
   */
940
  protected IdeSubLogger asSuccess(Step step) {
941

942
    if (step == null) {
2!
943
      return this.context.success();
4✔
944
    } else {
945
      return step.asSuccess();
×
946
    }
947
  }
948

949

950
  /**
951
   * @param step the {@link Step} to get {@link Step#asError() error logger} from. May be {@code null}.
952
   * @return the {@link IdeSubLogger} from {@link Step#asError()} or {@link IdeContext#error()} as fallback.
953
   */
954
  protected IdeSubLogger asError(Step step) {
955

956
    if (step == null) {
×
957
      return this.context.error();
×
958
    } else {
959
      return step.asError();
×
960
    }
961
  }
962
}
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

© 2025 Coveralls, Inc