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

devonfw / IDEasy / 20330510700

18 Dec 2025 08:19AM UTC coverage: 70.088% (+0.001%) from 70.087%
20330510700

push

github

web-flow
#1646 Implemented check for duplicate versions (#1662)

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

3981 of 6255 branches covered (63.65%)

Branch coverage included in aggregate %.

10188 of 13961 relevant lines covered (72.97%)

3.15 hits per line

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

75.56
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.Objects;
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.security.ToolVulnerabilities;
29
import com.devonfw.tools.ide.step.Step;
30
import com.devonfw.tools.ide.tool.repository.ToolRepository;
31
import com.devonfw.tools.ide.url.model.file.json.Cve;
32
import com.devonfw.tools.ide.url.model.file.json.ToolDependency;
33
import com.devonfw.tools.ide.url.model.file.json.ToolSecurity;
34
import com.devonfw.tools.ide.variable.IdeVariables;
35
import com.devonfw.tools.ide.version.GenericVersionRange;
36
import com.devonfw.tools.ide.version.VersionIdentifier;
37

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

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

46
  private final Set<Tag> tags;
47

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

51
  private Path executionDirectory;
52

53
  private MacOsHelper macOsHelper;
54

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

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

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

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

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

86
    return this.tool;
3✔
87
  }
88

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

94
    return this.tool;
3✔
95
  }
96

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

100
    return this.tags;
3✔
101
  }
102

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

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

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

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

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

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

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

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

143
  @Override
144
  public void run() {
145

146
    runTool(this.arguments.asList());
6✔
147
  }
1✔
148

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

310
  private void completeRequestInstalled(ToolInstallRequest request) {
311

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

330
  private void completeRequestRequested(ToolInstallRequest request) {
331

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

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

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

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

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

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

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

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

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

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

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

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

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

436
  /**
437
   * @param rootDir the {@link ToolInstallation#rootDir() top-level installation 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, VersionIdentifier version, boolean newInstallation,
446
      EnvironmentContext environmentContext, boolean additionalInstallation) {
447

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

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

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

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

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

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

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

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

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

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

551
    return true;
2✔
552
  }
553

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

564
    ToolEditionAndVersion requested = request.getRequested();
3✔
565
    VersionIdentifier resolvedVersion = requested.getResolvedVersion();
3✔
566
    if (request.isCveCheckDone()) {
3✔
567
      return resolvedVersion;
2✔
568
    }
569
    ToolEdition toolEdition = requested.getEdition();
3✔
570
    GenericVersionRange allowedVersions = requested.getVersion();
3✔
571
    boolean requireStableVersion = true;
2✔
572
    if (allowedVersions instanceof VersionIdentifier vi) {
6✔
573
      requireStableVersion = vi.isStable();
3✔
574
    }
575
    ToolSecurity toolSecurity = this.context.getDefaultToolRepository().findSecurity(this.tool, toolEdition.edition());
9✔
576
    double minSeverity = IdeVariables.CVE_MIN_SEVERITY.get(context);
7✔
577
    ToolVulnerabilities currentVulnerabilities = toolSecurity.findCves(resolvedVersion, this.context, minSeverity);
7✔
578
    ToolVersionChoice currentChoice = ToolVersionChoice.ofCurrent(requested, currentVulnerabilities);
4✔
579
    request.setCveCheckDone();
2✔
580
    if (currentChoice.logAndCheckIfEmpty(this.context)) {
5✔
581
      return resolvedVersion;
2✔
582
    }
583
    boolean alreadyInstalled = request.isAlreadyInstalled();
3✔
584
    boolean directForceInstall = this.context.isForceMode() && request.isDirect();
6!
585
    if (alreadyInstalled && !directForceInstall) {
2!
586
      // currently for a transitive dependency it does not make sense to suggest alternative versions, since the choice is not stored anywhere,
587
      // 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
588
      // (e.g. upgrade the tool with the dependency that is causing this).
589
      this.context.interaction("Please run 'ide -f install {}' to check for update suggestions!", this.tool);
×
590
      return resolvedVersion;
×
591
    }
592
    ToolVersionChoice latest = null;
2✔
593
    ToolVulnerabilities latestVulnerabilities = currentVulnerabilities;
2✔
594
    ToolVersionChoice nearest = null;
2✔
595
    ToolVulnerabilities nearestVulnerabilities = currentVulnerabilities;
2✔
596
    List<VersionIdentifier> toolVersions = getVersions();
3✔
597
    for (VersionIdentifier version : toolVersions) {
10✔
598

599
      if (Objects.equals(version, resolvedVersion)) {
4✔
600
        continue; // Skip the entire iteration for resolvedVersion
1✔
601
      }
602

603
      if (acceptVersion(version, allowedVersions, requireStableVersion)) {
5!
604
        ToolVulnerabilities newVulnerabilities = toolSecurity.findCves(version, this.context, minSeverity);
7✔
605
        if (newVulnerabilities.isSafer(latestVulnerabilities)) {
4✔
606
          // we found a better/safer version
607
          ToolEditionAndVersion toolEditionAndVersion = new ToolEditionAndVersion(toolEdition, version);
6✔
608
          if (version.isGreater(resolvedVersion)) {
4!
609
            latestVulnerabilities = newVulnerabilities;
2✔
610
            latest = ToolVersionChoice.ofLatest(toolEditionAndVersion, latestVulnerabilities);
4✔
611
            nearest = null;
3✔
612
          } else {
613
            nearestVulnerabilities = newVulnerabilities;
×
614
            nearest = ToolVersionChoice.ofNearest(toolEditionAndVersion, nearestVulnerabilities);
×
615
          }
616
        } else if (newVulnerabilities.isSaferOrEqual(nearestVulnerabilities)) {
5✔
617
          if (newVulnerabilities.isSafer(nearestVulnerabilities) || version.isGreater(resolvedVersion)) {
8!
618
            nearest = ToolVersionChoice.ofNearest(new ToolEditionAndVersion(toolEdition, version), newVulnerabilities);
8✔
619
          }
620
          nearestVulnerabilities = newVulnerabilities;
2✔
621
        }
622
      }
623
    }
1✔
624
    if ((latest == null) && (nearest == null)) {
2!
625
      this.context.warning(
×
626
          "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.");
627
      if (alreadyInstalled) {
×
628
        // we came here via "ide -f install ..." but no alternative is available
629
        return resolvedVersion;
×
630
      }
631
    }
632
    List<ToolVersionChoice> choices = new ArrayList<>();
4✔
633
    choices.add(currentChoice);
4✔
634
    boolean addSuggestions;
635
    if (this.context.isForceMode() && request.isDirect()) {
4!
636
      addSuggestions = true;
×
637
    } else {
638
      List<String> skipCveFixTools = IdeVariables.SKIP_CVE_FIX.get(this.context);
6✔
639
      addSuggestions = !skipCveFixTools.contains(this.tool);
8!
640
    }
641
    if (nearest != null) {
2!
642
      if (addSuggestions) {
2!
643
        choices.add(nearest);
4✔
644
      }
645
      nearest.logAndCheckIfEmpty(this.context);
5✔
646
    }
647
    if (latest != null) {
2!
648
      if (addSuggestions) {
2!
649
        choices.add(latest);
4✔
650
      }
651
      latest.logAndCheckIfEmpty(this.context);
5✔
652
    }
653
    ToolVersionChoice[] choicesArray = choices.toArray(ToolVersionChoice[]::new);
8✔
654
    this.context.warning(
4✔
655
        "Please note that by selecting an unsafe version to install, you accept the risk to be attacked.");
656
    ToolVersionChoice answer = this.context.question(choicesArray, "Which version do you want to install?");
9✔
657
    VersionIdentifier version = answer.toolEditionAndVersion().getResolvedVersion();
4✔
658
    requested.setResolvedVersion(version);
3✔
659
    return version;
2✔
660
  }
661

662
  private static boolean acceptVersion(VersionIdentifier version, GenericVersionRange allowedVersions, boolean requireStableVersion) {
663
    if (allowedVersions.isPattern() && !allowedVersions.contains(version)) {
3!
664
      return false;
×
665
    } else if (requireStableVersion && !version.isStable()) {
5!
666
      return false;
×
667
    }
668
    return true;
2✔
669
  }
670

671
  /**
672
   * @return the {@link MacOsHelper} instance.
673
   */
674
  protected MacOsHelper getMacOsHelper() {
675

676
    if (this.macOsHelper == null) {
3✔
677
      this.macOsHelper = new MacOsHelper(this.context);
7✔
678
    }
679
    return this.macOsHelper;
3✔
680
  }
681

682
  /**
683
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
684
   */
685
  public abstract VersionIdentifier getInstalledVersion();
686

687
  /**
688
   * @return {@code true} if this tool is installed, {@code false} otherwise.
689
   */
690
  public boolean isInstalled() {
691

692
    return getInstalledVersion() != null;
7✔
693
  }
694

695
  /**
696
   * @return the installed edition of this tool or {@code null} if not installed.
697
   */
698
  public abstract String getInstalledEdition();
699

700
  /**
701
   * Uninstalls the {@link #getName() tool}.
702
   */
703
  public abstract void uninstall();
704

705
  /**
706
   * @return the {@link ToolRepository}.
707
   */
708
  public ToolRepository getToolRepository() {
709

710
    return this.context.getDefaultToolRepository();
4✔
711
  }
712

713
  /**
714
   * List the available editions of this tool.
715
   */
716
  public void listEditions() {
717

718
    List<String> editions = getToolRepository().getSortedEditions(getName());
6✔
719
    for (String edition : editions) {
10✔
720
      this.context.info(edition);
4✔
721
    }
1✔
722
  }
1✔
723

724
  /**
725
   * List the available versions of this tool.
726
   */
727
  public void listVersions() {
728

729
    List<VersionIdentifier> versions = getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
730
    for (VersionIdentifier vi : versions) {
10✔
731
      this.context.info(vi.toString());
5✔
732
    }
1✔
733
  }
1✔
734

735
  /**
736
   * @return the {@link com.devonfw.tools.ide.tool.repository.DefaultToolRepository#getSortedVersions(String, String, ToolCommandlet) sorted versions} of this
737
   *     tool.
738
   */
739
  public List<VersionIdentifier> getVersions() {
740
    return getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
741
  }
742

743
  /**
744
   * Sets the tool version in the environment variable configuration file.
745
   *
746
   * @param version the version (pattern) to set.
747
   */
748
  public void setVersion(String version) {
749

750
    if ((version == null) || version.isBlank()) {
×
751
      throw new IllegalStateException("Version has to be specified!");
×
752
    }
753
    VersionIdentifier configuredVersion = VersionIdentifier.of(version);
×
754
    if (!configuredVersion.isPattern() && !configuredVersion.isValid()) {
×
755
      this.context.warning("Version {} seems to be invalid", version);
×
756
    }
757
    setVersion(configuredVersion, true);
×
758
  }
×
759

760
  /**
761
   * Sets the tool version in the environment variable configuration file.
762
   *
763
   * @param version the version to set. May also be a {@link VersionIdentifier#isPattern() version pattern}.
764
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
765
   */
766
  public void setVersion(VersionIdentifier version, boolean hint) {
767

768
    setVersion(version, hint, null);
5✔
769
  }
1✔
770

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

780
    String edition = getConfiguredEdition();
3✔
781
    ToolRepository toolRepository = getToolRepository();
3✔
782

783
    EnvironmentVariables variables = this.context.getVariables();
4✔
784
    if (destination == null) {
2✔
785
      //use default location
786
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
787
    }
788
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
789
    String name = EnvironmentVariables.getToolVersionVariable(this.tool);
4✔
790

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

805
  /**
806
   * Sets the tool edition in the environment variable configuration file.
807
   *
808
   * @param edition the edition to set.
809
   */
810
  public void setEdition(String edition) {
811

812
    setEdition(edition, true);
4✔
813
  }
1✔
814

815
  /**
816
   * Sets the tool edition in the environment variable configuration file.
817
   *
818
   * @param edition the edition to set
819
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
820
   */
821
  public void setEdition(String edition, boolean hint) {
822

823
    setEdition(edition, hint, null);
5✔
824
  }
1✔
825

826
  /**
827
   * Sets the tool edition in the environment variable configuration file.
828
   *
829
   * @param edition the edition to set
830
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
831
   * @param destination - the destination for the property to be set
832
   */
833
  public void setEdition(String edition, boolean hint, EnvironmentVariablesFiles destination) {
834

835
    if ((edition == null) || edition.isBlank()) {
5!
836
      throw new IllegalStateException("Edition has to be specified!");
×
837
    }
838

839
    if (destination == null) {
2✔
840
      //use default location
841
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
842
    }
843

844
    if (!getToolRepository().getSortedEditions(this.tool).contains(edition)) {
8✔
845
      this.context.warning("Edition {} seems to be invalid", edition);
10✔
846
    }
847
    EnvironmentVariables variables = this.context.getVariables();
4✔
848
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
849
    String name = EnvironmentVariables.getToolEditionVariable(this.tool);
4✔
850
    settingsVariables.set(name, edition, false);
6✔
851
    settingsVariables.save();
2✔
852

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

865
  /**
866
   * Runs the tool's help command to provide the user with usage information.
867
   */
868
  @Override
869
  public void printHelp(NlsBundle bundle) {
870

871
    super.printHelp(bundle);
3✔
872
    String toolHelpArgs = getToolHelpArguments();
3✔
873
    if (toolHelpArgs != null && getInstalledVersion() != null) {
5!
874
      ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.LOG_WARNING)
6✔
875
          .executable(Path.of(getBinaryName())).addArgs(toolHelpArgs);
13✔
876
      pc.run(ProcessMode.DEFAULT);
4✔
877
    }
878
  }
1✔
879

880
  /**
881
   * @return the tool's specific help command. Usually help, --help or -h. Return null if not applicable.
882
   */
883
  public String getToolHelpArguments() {
884

885
    return null;
×
886
  }
887

888
  /**
889
   * Creates a start script for the tool using the tool name.
890
   *
891
   * @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
892
   *     instead.
893
   * @param binary name of the binary to execute from the start script.
894
   */
895
  protected void createStartScript(Path targetDir, String binary) {
896

897
    createStartScript(targetDir, binary, false);
×
898
  }
×
899

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

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

936
  @Override
937
  public void reset() {
938
    super.reset();
2✔
939
    this.executionDirectory = null;
3✔
940
  }
1✔
941

942
  /**
943
   * @param step the {@link Step} to get {@link Step#asSuccess() success logger} from. May be {@code null}.
944
   * @return the {@link IdeSubLogger} from {@link Step#asSuccess()} or {@link IdeContext#success()} as fallback.
945
   */
946
  protected IdeSubLogger asSuccess(Step step) {
947

948
    if (step == null) {
2!
949
      return this.context.success();
4✔
950
    } else {
951
      return step.asSuccess();
×
952
    }
953
  }
954

955

956
  /**
957
   * @param step the {@link Step} to get {@link Step#asError() error logger} from. May be {@code null}.
958
   * @return the {@link IdeSubLogger} from {@link Step#asError()} or {@link IdeContext#error()} as fallback.
959
   */
960
  protected IdeSubLogger asError(Step step) {
961

962
    if (step == null) {
×
963
      return this.context.error();
×
964
    } else {
965
      return step.asError();
×
966
    }
967
  }
968
}
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