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

devonfw / IDEasy / 20021384931

08 Dec 2025 08:20AM UTC coverage: 69.924% (-0.08%) from 70.003%
20021384931

push

github

web-flow
#1640: remove post-install hook from terraform (#1641)

3924 of 6173 branches covered (63.57%)

Branch coverage included in aggregate %.

10049 of 13810 relevant lines covered (72.77%)

3.15 hits per line

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

75.94
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
    return createExistingToolInstallation(installed.getEdition().edition(), installed.getResolvedVersion(), request.getProcessContext(),
11✔
404
        request.isAdditionalInstallation());
1✔
405
  }
406

407
  /**
408
   * @param edition the {@link #getConfiguredEdition() edition}.
409
   * @param installedVersion the {@link #getConfiguredVersion() version}.
410
   * @param environmentContext the {@link EnvironmentContext}.
411
   * @param extraInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
412
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
413
   * @return the {@link ToolInstallation}.
414
   */
415
  protected ToolInstallation createExistingToolInstallation(String edition, VersionIdentifier installedVersion, EnvironmentContext environmentContext,
416
      boolean extraInstallation) {
417

418
    Path installationPath = getInstallationPath(edition, installedVersion);
5✔
419
    return createToolInstallation(installationPath, installedVersion, false, environmentContext, extraInstallation);
8✔
420
  }
421

422
  /**
423
   * @param rootDir the {@link ToolInstallation#rootDir() top-level installation directory}.
424
   * @param version the installed {@link VersionIdentifier}.
425
   * @param newInstallation {@link ToolInstallation#newInstallation() new installation} flag.
426
   * @param environmentContext the {@link EnvironmentContext}.
427
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
428
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
429
   * @return the {@link ToolInstallation}.
430
   */
431
  protected ToolInstallation createToolInstallation(Path rootDir, VersionIdentifier version, boolean newInstallation,
432
      EnvironmentContext environmentContext, boolean additionalInstallation) {
433

434
    Path linkDir = rootDir;
2✔
435
    Path binDir = rootDir;
2✔
436
    if (rootDir != null) {
2✔
437
      // on MacOS applications have a very strange structure - see JavaDoc of findLinkDir and ToolInstallation.linkDir for details.
438
      linkDir = getMacOsHelper().findLinkDir(rootDir, getBinaryName());
7✔
439
      binDir = this.context.getFileAccess().getBinPath(linkDir);
6✔
440
    }
441
    return createToolInstallation(rootDir, linkDir, binDir, version, newInstallation, environmentContext, additionalInstallation);
10✔
442
  }
443

444
  /**
445
   * @param rootDir the {@link ToolInstallation#rootDir() top-level installation directory}.
446
   * @param linkDir the {@link ToolInstallation#linkDir() link directory}.
447
   * @param binDir the {@link ToolInstallation#binDir() bin directory}.
448
   * @param version the installed {@link VersionIdentifier}.
449
   * @param newInstallation {@link ToolInstallation#newInstallation() new installation} flag.
450
   * @param environmentContext the {@link EnvironmentContext}.
451
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
452
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
453
   * @return the {@link ToolInstallation}.
454
   */
455
  protected ToolInstallation createToolInstallation(Path rootDir, Path linkDir, Path binDir, VersionIdentifier version, boolean newInstallation,
456
      EnvironmentContext environmentContext, boolean additionalInstallation) {
457

458
    if (linkDir != rootDir) {
3✔
459
      assert (!linkDir.equals(rootDir));
5!
460
      Path toolVersionFile = rootDir.resolve(IdeContext.FILE_SOFTWARE_VERSION);
4✔
461
      if (Files.exists(toolVersionFile)) {
5!
462
        this.context.getFileAccess().copy(toolVersionFile, linkDir, FileCopyMode.COPY_FILE_OVERRIDE);
7✔
463
      }
464
    }
465
    ToolInstallation toolInstallation = new ToolInstallation(rootDir, linkDir, binDir, version, newInstallation);
9✔
466
    setEnvironment(environmentContext, toolInstallation, additionalInstallation);
5✔
467
    return toolInstallation;
2✔
468
  }
469

470
  /**
471
   * Called if the tool {@link ToolInstallRequest#isAlreadyInstalled() is already installed in the correct edition and version} so we can skip the
472
   * installation.
473
   *
474
   * @param request the {@link ToolInstallRequest}.
475
   * @return the {@link ToolInstallation}.
476
   */
477
  protected ToolInstallation toolAlreadyInstalled(ToolInstallRequest request) {
478

479
    logToolAlreadyInstalled(request);
3✔
480
    cveCheck(request);
4✔
481
    postInstall(request);
3✔
482
    return createExistingToolInstallation(request);
4✔
483
  }
484

485
  /**
486
   * Log that the tool is already installed.
487
   *
488
   * @param request the {@link ToolInstallRequest}.
489
   */
490
  protected void logToolAlreadyInstalled(ToolInstallRequest request) {
491
    IdeSubLogger logger;
492
    if (request.isSilent()) {
3✔
493
      logger = this.context.debug();
5✔
494
    } else {
495
      logger = this.context.info();
4✔
496
    }
497
    ToolEditionAndVersion installed = request.getInstalled();
3✔
498
    logger.log("Version {} of tool {} is already installed", installed.getVersion(), installed.getEdition());
16✔
499
  }
1✔
500

501
  /**
502
   * Method to get the home path of the given {@link ToolInstallation}.
503
   *
504
   * @param toolInstallation the {@link ToolInstallation}.
505
   * @return the Path to the home of the tool
506
   */
507
  protected Path getToolHomePath(ToolInstallation toolInstallation) {
508
    return toolInstallation.linkDir();
3✔
509
  }
510

511
  /**
512
   * Method to set environment variables for the process context.
513
   *
514
   * @param environmentContext the {@link EnvironmentContext} where to {@link EnvironmentContext#withEnvVar(String, String) set environment variables} for
515
   *     this tool.
516
   * @param toolInstallation the {@link ToolInstallation}.
517
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
518
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
519
   */
520
  public void setEnvironment(EnvironmentContext environmentContext, ToolInstallation toolInstallation, boolean additionalInstallation) {
521

522
    String pathVariable = EnvironmentVariables.getToolVariablePrefix(this.tool) + "_HOME";
5✔
523
    Path toolHomePath = getToolHomePath(toolInstallation);
4✔
524
    if (toolHomePath != null) {
2✔
525
      environmentContext.withEnvVar(pathVariable, toolHomePath.toString());
6✔
526
    }
527
    if (additionalInstallation) {
2✔
528
      environmentContext.withPathEntry(toolInstallation.binDir());
5✔
529
    }
530
  }
1✔
531

532
  /**
533
   * @return {@code true} to extract (unpack) the downloaded binary file, {@code false} otherwise.
534
   */
535
  protected boolean isExtract() {
536

537
    return true;
2✔
538
  }
539

540
  /**
541
   * 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
542
   * something better, we will suggest this to the user and ask him to make his choice.
543
   *
544
   * @param request the {@link ToolInstallRequest}.
545
   * @return the {@link VersionIdentifier} to install. The will may be asked (unless {@code skipSuggestions} is {@code true}) and might choose a different
546
   *     version than the originally requested one.
547
   */
548
  protected VersionIdentifier cveCheck(ToolInstallRequest request) {
549

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

643
  private static boolean acceptVersion(VersionIdentifier version, GenericVersionRange allowedVersions, boolean requireStableVersion) {
644
    if (allowedVersions.isPattern() && !allowedVersions.contains(version)) {
3!
645
      return false;
×
646
    } else if (requireStableVersion && !version.isStable()) {
5!
647
      return false;
×
648
    }
649
    return true;
2✔
650
  }
651

652
  /**
653
   * @return the {@link MacOsHelper} instance.
654
   */
655
  protected MacOsHelper getMacOsHelper() {
656

657
    if (this.macOsHelper == null) {
3✔
658
      this.macOsHelper = new MacOsHelper(this.context);
7✔
659
    }
660
    return this.macOsHelper;
3✔
661
  }
662

663
  /**
664
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
665
   */
666
  public abstract VersionIdentifier getInstalledVersion();
667

668
  /**
669
   * @return {@code true} if this tool is installed, {@code false} otherwise.
670
   */
671
  public boolean isInstalled() {
672

673
    return getInstalledVersion() != null;
7✔
674
  }
675

676
  /**
677
   * @return the installed edition of this tool or {@code null} if not installed.
678
   */
679
  public abstract String getInstalledEdition();
680

681
  /**
682
   * Uninstalls the {@link #getName() tool}.
683
   */
684
  public abstract void uninstall();
685

686
  /**
687
   * @return the {@link ToolRepository}.
688
   */
689
  public ToolRepository getToolRepository() {
690

691
    return this.context.getDefaultToolRepository();
4✔
692
  }
693

694
  /**
695
   * List the available editions of this tool.
696
   */
697
  public void listEditions() {
698

699
    List<String> editions = getToolRepository().getSortedEditions(getName());
6✔
700
    for (String edition : editions) {
10✔
701
      this.context.info(edition);
4✔
702
    }
1✔
703
  }
1✔
704

705
  /**
706
   * List the available versions of this tool.
707
   */
708
  public void listVersions() {
709

710
    List<VersionIdentifier> versions = getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
711
    for (VersionIdentifier vi : versions) {
10✔
712
      this.context.info(vi.toString());
5✔
713
    }
1✔
714
  }
1✔
715

716
  /**
717
   * @return the {@link com.devonfw.tools.ide.tool.repository.DefaultToolRepository#getSortedVersions(String, String, ToolCommandlet) sorted versions} of this
718
   *     tool.
719
   */
720
  public List<VersionIdentifier> getVersions() {
721
    return getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
722
  }
723

724
  /**
725
   * Sets the tool version in the environment variable configuration file.
726
   *
727
   * @param version the version (pattern) to set.
728
   */
729
  public void setVersion(String version) {
730

731
    if ((version == null) || version.isBlank()) {
×
732
      throw new IllegalStateException("Version has to be specified!");
×
733
    }
734
    VersionIdentifier configuredVersion = VersionIdentifier.of(version);
×
735
    if (!configuredVersion.isPattern() && !configuredVersion.isValid()) {
×
736
      this.context.warning("Version {} seems to be invalid", version);
×
737
    }
738
    setVersion(configuredVersion, true);
×
739
  }
×
740

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

749
    setVersion(version, hint, null);
5✔
750
  }
1✔
751

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

761
    String edition = getConfiguredEdition();
3✔
762
    ToolRepository toolRepository = getToolRepository();
3✔
763

764
    EnvironmentVariables variables = this.context.getVariables();
4✔
765
    if (destination == null) {
2✔
766
      //use default location
767
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
768
    }
769
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
770
    String name = EnvironmentVariables.getToolVersionVariable(this.tool);
4✔
771

772
    toolRepository.resolveVersion(this.tool, edition, version, this); // verify that the version actually exists
8✔
773
    settingsVariables.set(name, version.toString(), false);
7✔
774
    settingsVariables.save();
2✔
775
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
776
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
777
      this.context.warning("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name,
13✔
778
          declaringVariables.getSource());
2✔
779
    }
780
    if (hint) {
2✔
781
      this.context.info("To install that version call the following command:");
4✔
782
      this.context.info("ide install {}", this.tool);
11✔
783
    }
784
  }
1✔
785

786
  /**
787
   * Sets the tool edition in the environment variable configuration file.
788
   *
789
   * @param edition the edition to set.
790
   */
791
  public void setEdition(String edition) {
792

793
    setEdition(edition, true);
4✔
794
  }
1✔
795

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

804
    setEdition(edition, hint, null);
5✔
805
  }
1✔
806

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

816
    if ((edition == null) || edition.isBlank()) {
5!
817
      throw new IllegalStateException("Edition has to be specified!");
×
818
    }
819

820
    if (destination == null) {
2✔
821
      //use default location
822
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
823
    }
824

825
    if (!getToolRepository().getSortedEditions(this.tool).contains(edition)) {
8✔
826
      this.context.warning("Edition {} seems to be invalid", edition);
10✔
827
    }
828
    EnvironmentVariables variables = this.context.getVariables();
4✔
829
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
830
    String name = EnvironmentVariables.getToolEditionVariable(this.tool);
4✔
831
    settingsVariables.set(name, edition, false);
6✔
832
    settingsVariables.save();
2✔
833

834
    this.context.info("{}={} has been set in {}", name, edition, settingsVariables.getSource());
19✔
835
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
836
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
837
      this.context.warning("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name,
13✔
838
          declaringVariables.getSource());
2✔
839
    }
840
    if (hint) {
2!
841
      this.context.info("To install that edition call the following command:");
4✔
842
      this.context.info("ide install {}", this.tool);
11✔
843
    }
844
  }
1✔
845

846
  /**
847
   * Runs the tool's help command to provide the user with usage information.
848
   */
849
  @Override
850
  public void printHelp(NlsBundle bundle) {
851

852
    super.printHelp(bundle);
3✔
853
    String toolHelpArgs = getToolHelpArguments();
3✔
854
    if (toolHelpArgs != null && getInstalledVersion() != null) {
5!
855
      ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.LOG_WARNING)
6✔
856
          .executable(Path.of(getBinaryName())).addArgs(toolHelpArgs);
13✔
857
      pc.run(ProcessMode.DEFAULT);
4✔
858
    }
859
  }
1✔
860

861
  /**
862
   * @return the tool's specific help command. Usually help, --help or -h. Return null if not applicable.
863
   */
864
  public String getToolHelpArguments() {
865

866
    return null;
×
867
  }
868

869
  /**
870
   * Creates a start script for the tool using the tool name.
871
   *
872
   * @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
873
   *     instead.
874
   * @param binary name of the binary to execute from the start script.
875
   */
876
  protected void createStartScript(Path targetDir, String binary) {
877

878
    createStartScript(targetDir, binary, false);
×
879
  }
×
880

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

891
    Path binFolder = targetDir.resolve("bin");
×
892
    if (!Files.exists(binFolder)) {
×
893
      if (this.context.getSystemInfo().isMac()) {
×
894
        MacOsHelper macOsHelper = getMacOsHelper();
×
895
        Path appDir = macOsHelper.findAppDir(targetDir);
×
896
        binFolder = macOsHelper.findLinkDir(appDir, binary);
×
897
      } else {
×
898
        binFolder = targetDir;
×
899
      }
900
      assert (Files.exists(binFolder));
×
901
    }
902
    Path bashFile = binFolder.resolve(getName());
×
903
    String bashFileContentStart = "#!/usr/bin/env bash\n\"$(dirname \"$0\")/";
×
904
    String bashFileContentEnd = "\" $@";
×
905
    if (background) {
×
906
      bashFileContentEnd += " &";
×
907
    }
908
    try {
909
      Files.writeString(bashFile, bashFileContentStart + binary + bashFileContentEnd);
×
910
    } catch (IOException e) {
×
911
      throw new RuntimeException(e);
×
912
    }
×
913
    assert (Files.exists(bashFile));
×
914
    context.getFileAccess().makeExecutable(bashFile);
×
915
  }
×
916

917
  @Override
918
  public void reset() {
919
    super.reset();
2✔
920
    this.executionDirectory = null;
3✔
921
  }
1✔
922

923
  /**
924
   * @param step the {@link Step} to get {@link Step#asSuccess() success logger} from. May be {@code null}.
925
   * @return the {@link IdeSubLogger} from {@link Step#asSuccess()} or {@link IdeContext#success()} as fallback.
926
   */
927
  protected IdeSubLogger asSuccess(Step step) {
928

929
    if (step == null) {
2!
930
      return this.context.success();
4✔
931
    } else {
932
      return step.asSuccess();
×
933
    }
934
  }
935

936

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

943
    if (step == null) {
×
944
      return this.context.error();
×
945
    } else {
946
      return step.asError();
×
947
    }
948
  }
949
}
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