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

devonfw / IDEasy / 21099258243

17 Jan 2026 06:52PM UTC coverage: 70.349% (-0.006%) from 70.355%
21099258243

push

github

hohwille
Merge branch 'maybeec-feature/#1667-fix-env-install-trigger'

4017 of 6298 branches covered (63.78%)

Branch coverage included in aggregate %.

10444 of 14258 relevant lines covered (73.25%)

3.17 hits per line

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

73.63
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
import java.util.regex.Matcher;
11
import java.util.regex.Pattern;
12

13
import com.devonfw.tools.ide.commandlet.Commandlet;
14
import com.devonfw.tools.ide.common.Tag;
15
import com.devonfw.tools.ide.common.Tags;
16
import com.devonfw.tools.ide.context.IdeContext;
17
import com.devonfw.tools.ide.environment.EnvironmentVariables;
18
import com.devonfw.tools.ide.environment.EnvironmentVariablesFiles;
19
import com.devonfw.tools.ide.io.FileCopyMode;
20
import com.devonfw.tools.ide.log.IdeSubLogger;
21
import com.devonfw.tools.ide.nls.NlsBundle;
22
import com.devonfw.tools.ide.os.MacOsHelper;
23
import com.devonfw.tools.ide.process.EnvironmentContext;
24
import com.devonfw.tools.ide.process.ProcessContext;
25
import com.devonfw.tools.ide.process.ProcessErrorHandling;
26
import com.devonfw.tools.ide.process.ProcessMode;
27
import com.devonfw.tools.ide.process.ProcessResult;
28
import com.devonfw.tools.ide.property.StringProperty;
29
import com.devonfw.tools.ide.security.ToolVersionChoice;
30
import com.devonfw.tools.ide.security.ToolVulnerabilities;
31
import com.devonfw.tools.ide.step.Step;
32
import com.devonfw.tools.ide.tool.repository.ToolRepository;
33
import com.devonfw.tools.ide.url.model.file.json.Cve;
34
import com.devonfw.tools.ide.url.model.file.json.ToolDependency;
35
import com.devonfw.tools.ide.url.model.file.json.ToolSecurity;
36
import com.devonfw.tools.ide.variable.IdeVariables;
37
import com.devonfw.tools.ide.version.GenericVersionRange;
38
import com.devonfw.tools.ide.version.VersionIdentifier;
39

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

45
  /** @see #getName() */
46
  protected final String tool;
47

48
  private final Set<Tag> tags;
49

50
  /** The commandline arguments to pass to the tool. */
51
  public final StringProperty arguments;
52

53
  private Path executionDirectory;
54

55
  private MacOsHelper macOsHelper;
56

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

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

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

79
    add(this.arguments);
5✔
80
  }
1✔
81

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

88
    return this.tool;
3✔
89
  }
90

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

96
    return this.tool;
3✔
97
  }
98

99
  /**
100
   * @return the {@link Path} to the installed {@link #getBinaryName() binary} or {@code null} if not found on the
101
   *     {@link com.devonfw.tools.ide.common.SystemPath}.
102
   */
103
  protected Path getBinaryExecutable() {
104

105
    Path binary = this.context.getPath().findBinaryPathByName(getBinaryName());
×
106
    if (binary.getParent() == null) {
×
107
      return null;
×
108
    }
109
    return binary;
×
110
  }
111

112
  @Override
113
  public final Set<Tag> getTags() {
114

115
    return this.tags;
3✔
116
  }
117

118
  /**
119
   * @return the execution directory where the tool will be executed. Will be {@code null} by default leading to execution in the users current working
120
   *     directory where IDEasy was called.
121
   * @see #setExecutionDirectory(Path)
122
   */
123
  public Path getExecutionDirectory() {
124
    return this.executionDirectory;
×
125
  }
126

127
  /**
128
   * @param executionDirectory the new value of {@link #getExecutionDirectory()}.
129
   */
130
  public void setExecutionDirectory(Path executionDirectory) {
131
    this.executionDirectory = executionDirectory;
×
132
  }
×
133

134
  /**
135
   * @return the {@link EnvironmentVariables#getToolVersion(String) tool version}.
136
   */
137
  public VersionIdentifier getConfiguredVersion() {
138

139
    return this.context.getVariables().getToolVersion(getName());
7✔
140
  }
141

142
  /**
143
   * @return the {@link EnvironmentVariables#getToolEdition(String) tool edition}.
144
   */
145
  public String getConfiguredEdition() {
146

147
    return this.context.getVariables().getToolEdition(getName());
7✔
148
  }
149

150
  /**
151
   * @return the {@link ToolEdition} with {@link #getName() tool} with its {@link #getConfiguredEdition() edition}.
152
   */
153
  protected final ToolEdition getToolWithConfiguredEdition() {
154

155
    return new ToolEdition(this.tool, getConfiguredEdition());
8✔
156
  }
157

158
  @Override
159
  public void run() {
160

161
    runTool(this.arguments.asList());
6✔
162
  }
1✔
163

164
  /**
165
   * @param args the command-line arguments to run the tool.
166
   * @return the {@link ProcessResult result}.
167
   * @see ToolCommandlet#runTool(ProcessMode, GenericVersionRange, List)
168
   */
169
  public ProcessResult runTool(List<String> args) {
170

171
    return runTool(ProcessMode.DEFAULT, null, args);
6✔
172
  }
173

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

185
    return runTool(processMode, toolVersion, ProcessErrorHandling.THROW_CLI, args);
7✔
186
  }
187

188
  /**
189
   * Ensures the tool is installed and then runs this tool with the given arguments.
190
   *
191
   * @param processMode the {@link ProcessMode}. Should typically be {@link ProcessMode#DEFAULT} or {@link ProcessMode#BACKGROUND}.
192
   * @param toolVersion the explicit {@link GenericVersionRange version} to run. Typically {@code null} to run the
193
   *     {@link #getConfiguredVersion() configured version}. Otherwise, the specified version will be used (from the software repository, if not compatible).
194
   * @param errorHandling the {@link ProcessErrorHandling}.
195
   * @param args the command-line arguments to run the tool.
196
   * @return the {@link ProcessResult result}.
197
   */
198
  public ProcessResult runTool(ProcessMode processMode, GenericVersionRange toolVersion, ProcessErrorHandling errorHandling, List<String> args) {
199

200
    ProcessContext pc = this.context.newProcess().errorHandling(errorHandling);
6✔
201
    ToolInstallRequest request = new ToolInstallRequest(true);
5✔
202
    if (toolVersion != null) {
2!
203
      request.setRequested(new ToolEditionAndVersion(toolVersion));
×
204
    }
205
    request.setProcessContext(pc);
3✔
206
    return runTool(request, processMode, args);
6✔
207
  }
208

209
  /**
210
   * Ensures the tool is installed and then runs this tool with the given arguments.
211
   *
212
   * @param request the {@link ToolInstallRequest}.
213
   * @param processMode the {@link ProcessMode}. Should typically be {@link ProcessMode#DEFAULT} or {@link ProcessMode#BACKGROUND}.
214
   * @param args the command-line arguments to run the tool.
215
   * @return the {@link ProcessResult result}.
216
   */
217
  public ProcessResult runTool(ToolInstallRequest request, ProcessMode processMode, List<String> args) {
218

219
    if (request.isCveCheckDone()) {
3!
220
      // if the CVE check has already been done, we can assume that the install(request) has already been called before
221
      // most likely a postInstall* method was overridden calling this method with the same request what is a programming error
222
      // we render this warning so the error gets detected and can be fixed but we do not block the user by skipping the installation.
223
      this.context.warning().log(new RuntimeException(), "Preventing infinity loop during installation of {}", request.getRequested());
×
224
    } else {
225
      install(request);
4✔
226
    }
227
    return runTool(request.getProcessContext(), processMode, args);
7✔
228
  }
229

230
  /**
231
   * @param pc the {@link ProcessContext}.
232
   * @param processMode the {@link ProcessMode}. Should typically be {@link ProcessMode#DEFAULT} or {@link ProcessMode#BACKGROUND}.
233
   * @param args the command-line arguments to run the tool.
234
   * @return the {@link ProcessResult result}.
235
   */
236
  public ProcessResult runTool(ProcessContext pc, ProcessMode processMode, List<String> args) {
237

238
    if (this.executionDirectory != null) {
3!
239
      pc.directory(this.executionDirectory);
×
240
    }
241
    configureToolBinary(pc, processMode);
4✔
242
    configureToolArgs(pc, processMode, args);
5✔
243
    return pc.run(processMode);
4✔
244
  }
245

246
  /**
247
   * @param pc the {@link ProcessContext}.
248
   * @param processMode the {@link ProcessMode}.
249
   */
250
  protected void configureToolBinary(ProcessContext pc, ProcessMode processMode) {
251

252
    pc.executable(Path.of(getBinaryName()));
8✔
253
  }
1✔
254

255
  /**
256
   * @param pc the {@link ProcessContext}.
257
   * @param processMode the {@link ProcessMode}.
258
   * @param args the command-line arguments to {@link ProcessContext#addArgs(List) add}.
259
   */
260
  protected void configureToolArgs(ProcessContext pc, ProcessMode processMode, List<String> args) {
261

262
    pc.addArgs(args);
4✔
263
  }
1✔
264

265
  /**
266
   * Installs or updates the managed {@link #getName() tool}.
267
   *
268
   * @return the {@link ToolInstallation}.
269
   */
270
  public ToolInstallation install() {
271

272
    return install(true);
4✔
273
  }
274

275
  /**
276
   * Performs the installation of the {@link #getName() tool} managed by this {@link com.devonfw.tools.ide.commandlet.Commandlet}.
277
   *
278
   * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
279
   * @return the {@link ToolInstallation}.
280
   */
281
  public ToolInstallation install(boolean silent) {
282
    return install(new ToolInstallRequest(silent));
7✔
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
  public ToolInstallation install(ToolInstallRequest request) {
292

293
    completeRequest(request);
3✔
294
    if (request.isInstallLoop(this.context)) {
5!
295
      return toolAlreadyInstalled(request);
×
296
    }
297
    return doInstall(request);
4✔
298
  }
299

300
  /**
301
   * Performs the installation (install, update, downgrade) of the {@link #getName() tool} managed by this {@link ToolCommandlet}.
302
   *
303
   * @param request the {@link ToolInstallRequest}.
304
   * @return the {@link ToolInstallation}.
305
   */
306
  protected abstract ToolInstallation doInstall(ToolInstallRequest request);
307

308
  /**
309
   * @param request the {@link ToolInstallRequest} to complete (fill values that are currently {@code null}).
310
   */
311
  protected void completeRequest(ToolInstallRequest request) {
312

313
    completeRequestInstalled(request);
3✔
314
    completeRequestRequested(request); // depends on completeRequestInstalled
3✔
315
    completeRequestProcessContext(request);
3✔
316
    completeRequestToolPath(request);
3✔
317
  }
1✔
318

319
  private void completeRequestProcessContext(ToolInstallRequest request) {
320
    if (request.getProcessContext() == null) {
3✔
321
      ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_CLI);
6✔
322
      request.setProcessContext(pc);
3✔
323
    }
324
  }
1✔
325

326
  private void completeRequestInstalled(ToolInstallRequest request) {
327

328
    ToolEditionAndVersion installedToolVersion = request.getInstalled();
3✔
329
    if (installedToolVersion == null) {
2✔
330
      installedToolVersion = new ToolEditionAndVersion((GenericVersionRange) null);
6✔
331
      request.setInstalled(installedToolVersion);
3✔
332
    }
333
    Path toolPath = request.getToolPath();
3✔
334
    if (installedToolVersion.getVersion() == null) {
3✔
335
      VersionIdentifier installedVersion;
336
      if ((toolPath != null) && (this instanceof LocalToolCommandlet ltc)) {
10!
337
        installedVersion = ltc.getInstalledVersion(toolPath);
5✔
338
      } else {
339
        installedVersion = getInstalledVersion();
3✔
340
      }
341
      if (installedVersion == null) {
2✔
342
        return;
1✔
343
      }
344
      installedToolVersion.setVersion(installedVersion);
3✔
345
    }
346
    if (installedToolVersion.getEdition() == null) {
3✔
347
      String installedEdition;
348
      if ((toolPath != null) && (this instanceof LocalToolCommandlet ltc)) {
2!
349
        installedEdition = ltc.getInstalledEdition(toolPath);
×
350
      } else {
351
        installedEdition = getInstalledEdition();
3✔
352
      }
353
      installedToolVersion.setEdition(new ToolEdition(this.tool, installedEdition));
8✔
354
    }
355
    assert installedToolVersion.getResolvedVersion() != null;
4!
356
  }
1✔
357

358
  private void completeRequestRequested(ToolInstallRequest request) {
359

360
    ToolEdition edition;
361
    ToolEditionAndVersion requested = request.getRequested();
3✔
362
    if (requested == null) {
2✔
363
      edition = new ToolEdition(this.tool, getConfiguredEdition());
8✔
364
      requested = new ToolEditionAndVersion(edition);
5✔
365
      request.setRequested(requested);
4✔
366
    } else {
367
      edition = requested.getEdition();
3✔
368
      if (edition == null) {
2✔
369
        edition = new ToolEdition(this.tool, getConfiguredEdition());
8✔
370
        requested.setEdition(edition);
3✔
371
      }
372
    }
373
    GenericVersionRange version = requested.getVersion();
3✔
374
    if (version == null) {
2✔
375
      version = getConfiguredVersion();
3✔
376
      requested.setVersion(version);
3✔
377
    }
378
    VersionIdentifier resolvedVersion = requested.getResolvedVersion();
3✔
379
    if (resolvedVersion == null) {
2✔
380
      if (this.context.isSkipUpdatesMode()) {
4✔
381
        ToolEditionAndVersion installed = request.getInstalled();
3✔
382
        if (installed != null) {
2!
383
          VersionIdentifier installedVersion = installed.getResolvedVersion();
3✔
384
          if (version.contains(installedVersion)) {
4✔
385
            resolvedVersion = installedVersion;
2✔
386
          }
387
        }
388
      }
389
      if (resolvedVersion == null) {
2✔
390
        resolvedVersion = getToolRepository().resolveVersion(this.tool, edition.edition(), version, this);
10✔
391
      }
392
      requested.setResolvedVersion(resolvedVersion);
3✔
393
    }
394
  }
1✔
395

396
  private void completeRequestToolPath(ToolInstallRequest request) {
397

398
    Path toolPath = request.getToolPath();
3✔
399
    if (toolPath == null) {
2✔
400
      toolPath = getToolPath();
3✔
401
      request.setToolPath(toolPath);
3✔
402
    }
403
  }
1✔
404

405
  /**
406
   * @return the {@link Path} where the tool is located (installed). Will be {@code null} for global tools that do not know the {@link Path} since it is
407
   *     determined by the installer.
408
   */
409
  public Path getToolPath() {
410
    return null;
×
411
  }
412

413
  /**
414
   * This method is called after a tool was requested to be installed or updated.
415
   *
416
   * @param request {@code true} the {@link ToolInstallRequest}.
417
   */
418
  protected void postInstall(ToolInstallRequest request) {
419

420
    if (!request.isAlreadyInstalled()) {
3✔
421
      postInstallOnNewInstallation(request);
3✔
422
    }
423
  }
1✔
424

425
  /**
426
   * This method is called after a tool was requested to be installed or updated and a new installation was performed.
427
   *
428
   * @param request {@code true} the {@link ToolInstallRequest}.
429
   */
430
  protected void postInstallOnNewInstallation(ToolInstallRequest request) {
431

432
    // nothing to do by default
433
  }
1✔
434

435
  /**
436
   * @param edition the {@link #getInstalledEdition() edition}.
437
   * @param version the {@link #getInstalledVersion() version}.
438
   * @return the {@link Path} where this tool is installed (physically) or {@code null} if not available.
439
   */
440
  protected abstract Path getInstallationPath(String edition, VersionIdentifier version);
441

442
  /**
443
   * @param request the {@link ToolInstallRequest}.
444
   * @return the existing {@link ToolInstallation}.
445
   */
446
  protected ToolInstallation createExistingToolInstallation(ToolInstallRequest request) {
447

448
    ToolEditionAndVersion installed = request.getInstalled();
3✔
449

450
    String edition = this.tool;
3✔
451
    VersionIdentifier resolvedVersion = VersionIdentifier.LATEST;
2✔
452

453
    if (installed != null) {
2!
454
      if (installed.getEdition() != null) {
3!
455
        edition = installed.getEdition().edition();
4✔
456
      }
457
      if (installed.getResolvedVersion() != null) {
3!
458
        resolvedVersion = installed.getResolvedVersion();
3✔
459
      }
460
    }
461

462
    return createExistingToolInstallation(edition, resolvedVersion, request.getProcessContext(),
8✔
463
        request.isAdditionalInstallation());
1✔
464
  }
465

466
  /**
467
   * @param edition the {@link #getConfiguredEdition() edition}.
468
   * @param installedVersion the {@link #getConfiguredVersion() version}.
469
   * @param environmentContext the {@link EnvironmentContext}.
470
   * @param extraInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
471
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
472
   * @return the {@link ToolInstallation}.
473
   */
474
  protected ToolInstallation createExistingToolInstallation(String edition, VersionIdentifier installedVersion, EnvironmentContext environmentContext,
475
      boolean extraInstallation) {
476

477
    Path installationPath = getInstallationPath(edition, installedVersion);
5✔
478
    return createToolInstallation(installationPath, installedVersion, false, environmentContext, extraInstallation);
8✔
479
  }
480

481
  /**
482
   * @param rootDir the {@link ToolInstallation#rootDir() top-level installation directory}.
483
   * @param version the installed {@link VersionIdentifier}.
484
   * @param newInstallation {@link ToolInstallation#newInstallation() new installation} flag.
485
   * @param environmentContext the {@link EnvironmentContext}.
486
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
487
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
488
   * @return the {@link ToolInstallation}.
489
   */
490
  protected ToolInstallation createToolInstallation(Path rootDir, VersionIdentifier version, boolean newInstallation,
491
      EnvironmentContext environmentContext, boolean additionalInstallation) {
492

493
    Path linkDir = rootDir;
2✔
494
    Path binDir = rootDir;
2✔
495
    if (rootDir != null) {
2!
496
      // on MacOS applications have a very strange structure - see JavaDoc of findLinkDir and ToolInstallation.linkDir for details.
497
      linkDir = getMacOsHelper().findLinkDir(rootDir, getBinaryName());
7✔
498
      binDir = this.context.getFileAccess().getBinPath(linkDir);
6✔
499
    }
500
    return createToolInstallation(rootDir, linkDir, binDir, version, newInstallation, environmentContext, additionalInstallation);
10✔
501
  }
502

503
  /**
504
   * @param rootDir the {@link ToolInstallation#rootDir() top-level installation directory}.
505
   * @param linkDir the {@link ToolInstallation#linkDir() link directory}.
506
   * @param binDir the {@link ToolInstallation#binDir() bin directory}.
507
   * @param version the installed {@link VersionIdentifier}.
508
   * @param newInstallation {@link ToolInstallation#newInstallation() new installation} flag.
509
   * @param environmentContext the {@link EnvironmentContext}.
510
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
511
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
512
   * @return the {@link ToolInstallation}.
513
   */
514
  protected ToolInstallation createToolInstallation(Path rootDir, Path linkDir, Path binDir, VersionIdentifier version, boolean newInstallation,
515
      EnvironmentContext environmentContext, boolean additionalInstallation) {
516

517
    if (linkDir != rootDir) {
3✔
518
      assert (!linkDir.equals(rootDir));
5!
519
      Path toolVersionFile = rootDir.resolve(IdeContext.FILE_SOFTWARE_VERSION);
4✔
520
      if (Files.exists(toolVersionFile)) {
5!
521
        this.context.getFileAccess().copy(toolVersionFile, linkDir, FileCopyMode.COPY_FILE_OVERRIDE);
7✔
522
      }
523
    }
524
    ToolInstallation toolInstallation = new ToolInstallation(rootDir, linkDir, binDir, version, newInstallation);
9✔
525
    setEnvironment(environmentContext, toolInstallation, additionalInstallation);
5✔
526
    return toolInstallation;
2✔
527
  }
528

529
  /**
530
   * Called if the tool {@link ToolInstallRequest#isAlreadyInstalled() is already installed in the correct edition and version} so we can skip the
531
   * installation.
532
   *
533
   * @param request the {@link ToolInstallRequest}.
534
   * @return the {@link ToolInstallation}.
535
   */
536
  protected ToolInstallation toolAlreadyInstalled(ToolInstallRequest request) {
537

538
    logToolAlreadyInstalled(request);
3✔
539
    cveCheck(request);
4✔
540
    postInstall(request);
3✔
541
    return createExistingToolInstallation(request);
4✔
542
  }
543

544
  /**
545
   * Log that the tool is already installed.
546
   *
547
   * @param request the {@link ToolInstallRequest}.
548
   */
549
  protected void logToolAlreadyInstalled(ToolInstallRequest request) {
550
    IdeSubLogger logger;
551
    if (request.isSilent()) {
3✔
552
      logger = this.context.debug();
5✔
553
    } else {
554
      logger = this.context.info();
4✔
555
    }
556
    ToolEditionAndVersion installed = request.getInstalled();
3✔
557
    logger.log("Version {} of tool {} is already installed", installed.getVersion(), installed.getEdition());
16✔
558
  }
1✔
559

560
  /**
561
   * Method to get the home path of the given {@link ToolInstallation}.
562
   *
563
   * @param toolInstallation the {@link ToolInstallation}.
564
   * @return the Path to the home of the tool
565
   */
566
  protected Path getToolHomePath(ToolInstallation toolInstallation) {
567
    return toolInstallation.linkDir();
3✔
568
  }
569

570
  /**
571
   * Method to set environment variables for the process context.
572
   *
573
   * @param environmentContext the {@link EnvironmentContext} where to {@link EnvironmentContext#withEnvVar(String, String) set environment variables} for
574
   *     this tool.
575
   * @param toolInstallation the {@link ToolInstallation}.
576
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
577
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
578
   */
579
  public void setEnvironment(EnvironmentContext environmentContext, ToolInstallation toolInstallation, boolean additionalInstallation) {
580

581
    String pathVariable = EnvironmentVariables.getToolVariablePrefix(this.tool) + "_HOME";
5✔
582
    Path toolHomePath = getToolHomePath(toolInstallation);
4✔
583
    if (toolHomePath != null) {
2!
584
      environmentContext.withEnvVar(pathVariable, toolHomePath.toString());
6✔
585
    }
586
    if (additionalInstallation) {
2✔
587
      environmentContext.withPathEntry(toolInstallation.binDir());
5✔
588
    }
589
  }
1✔
590

591
  /**
592
   * @return {@code true} to extract (unpack) the downloaded binary file, {@code false} otherwise.
593
   */
594
  protected boolean isExtract() {
595

596
    return true;
2✔
597
  }
598

599
  /**
600
   * 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
601
   * something better, we will suggest this to the user and ask him to make his choice.
602
   *
603
   * @param request the {@link ToolInstallRequest}.
604
   * @return the {@link VersionIdentifier} to install. The will may be asked (unless {@code skipSuggestions} is {@code true}) and might choose a different
605
   *     version than the originally requested one.
606
   */
607
  protected VersionIdentifier cveCheck(ToolInstallRequest request) {
608

609
    ToolEditionAndVersion requested = request.getRequested();
3✔
610
    VersionIdentifier resolvedVersion = requested.getResolvedVersion();
3✔
611
    if (request.isCveCheckDone()) {
3✔
612
      return resolvedVersion;
2✔
613
    }
614
    ToolEdition toolEdition = requested.getEdition();
3✔
615
    GenericVersionRange allowedVersions = requested.getVersion();
3✔
616
    boolean requireStableVersion = true;
2✔
617
    if (allowedVersions instanceof VersionIdentifier vi) {
6✔
618
      requireStableVersion = vi.isStable();
3✔
619
    }
620
    ToolSecurity toolSecurity = this.context.getDefaultToolRepository().findSecurity(this.tool, toolEdition.edition());
9✔
621
    double minSeverity = IdeVariables.CVE_MIN_SEVERITY.get(context);
7✔
622
    ToolVulnerabilities currentVulnerabilities = toolSecurity.findCves(resolvedVersion, this.context, minSeverity);
7✔
623
    ToolVersionChoice currentChoice = ToolVersionChoice.ofCurrent(requested, currentVulnerabilities);
4✔
624
    request.setCveCheckDone();
2✔
625
    if (currentChoice.logAndCheckIfEmpty(this.context)) {
5✔
626
      return resolvedVersion;
2✔
627
    }
628
    boolean alreadyInstalled = request.isAlreadyInstalled();
3✔
629
    boolean directForceInstall = this.context.isForceMode() && request.isDirect();
6!
630
    if (alreadyInstalled && !directForceInstall) {
2!
631
      // currently for a transitive dependency it does not make sense to suggest alternative versions, since the choice is not stored anywhere,
632
      // 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
633
      // (e.g. upgrade the tool with the dependency that is causing this).
634
      this.context.interaction("Please run 'ide -f install {}' to check for update suggestions!", this.tool);
×
635
      return resolvedVersion;
×
636
    }
637
    ToolVersionChoice latest = null;
2✔
638
    ToolVulnerabilities latestVulnerabilities = currentVulnerabilities;
2✔
639
    ToolVersionChoice nearest = null;
2✔
640
    ToolVulnerabilities nearestVulnerabilities = currentVulnerabilities;
2✔
641
    List<VersionIdentifier> toolVersions = getVersions();
3✔
642
    for (VersionIdentifier version : toolVersions) {
10✔
643

644
      if (Objects.equals(version, resolvedVersion)) {
4✔
645
        continue; // Skip the entire iteration for resolvedVersion
1✔
646
      }
647

648
      if (acceptVersion(version, allowedVersions, requireStableVersion)) {
5!
649
        ToolVulnerabilities newVulnerabilities = toolSecurity.findCves(version, this.context, minSeverity);
7✔
650
        if (newVulnerabilities.isSafer(latestVulnerabilities)) {
4✔
651
          // we found a better/safer version
652
          ToolEditionAndVersion toolEditionAndVersion = new ToolEditionAndVersion(toolEdition, version);
6✔
653
          if (version.isGreater(resolvedVersion)) {
4!
654
            latestVulnerabilities = newVulnerabilities;
2✔
655
            latest = ToolVersionChoice.ofLatest(toolEditionAndVersion, latestVulnerabilities);
4✔
656
            nearest = null;
3✔
657
          } else {
658
            nearestVulnerabilities = newVulnerabilities;
×
659
            nearest = ToolVersionChoice.ofNearest(toolEditionAndVersion, nearestVulnerabilities);
×
660
          }
661
        } else if (newVulnerabilities.isSaferOrEqual(nearestVulnerabilities)) {
5✔
662
          if (newVulnerabilities.isSafer(nearestVulnerabilities) || version.isGreater(resolvedVersion)) {
8!
663
            nearest = ToolVersionChoice.ofNearest(new ToolEditionAndVersion(toolEdition, version), newVulnerabilities);
8✔
664
          }
665
          nearestVulnerabilities = newVulnerabilities;
2✔
666
        }
667
      }
668
    }
1✔
669
    if ((latest == null) && (nearest == null)) {
2!
670
      this.context.warning(
×
671
          "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.");
672
      if (alreadyInstalled) {
×
673
        // we came here via "ide -f install ..." but no alternative is available
674
        return resolvedVersion;
×
675
      }
676
    }
677
    List<ToolVersionChoice> choices = new ArrayList<>();
4✔
678
    choices.add(currentChoice);
4✔
679
    boolean addSuggestions;
680
    if (this.context.isForceMode() && request.isDirect()) {
4!
681
      addSuggestions = true;
×
682
    } else {
683
      List<String> skipCveFixTools = IdeVariables.SKIP_CVE_FIX.get(this.context);
6✔
684
      addSuggestions = !skipCveFixTools.contains(this.tool);
8!
685
    }
686
    if (nearest != null) {
2!
687
      if (addSuggestions) {
2!
688
        choices.add(nearest);
4✔
689
      }
690
      nearest.logAndCheckIfEmpty(this.context);
5✔
691
    }
692
    if (latest != null) {
2!
693
      if (addSuggestions) {
2!
694
        choices.add(latest);
4✔
695
      }
696
      latest.logAndCheckIfEmpty(this.context);
5✔
697
    }
698
    ToolVersionChoice[] choicesArray = choices.toArray(ToolVersionChoice[]::new);
8✔
699
    this.context.warning(
4✔
700
        "Please note that by selecting an unsafe version to install, you accept the risk to be attacked.");
701
    ToolVersionChoice answer = this.context.question(choicesArray, "Which version do you want to install?");
9✔
702
    VersionIdentifier version = answer.toolEditionAndVersion().getResolvedVersion();
4✔
703
    requested.setResolvedVersion(version);
3✔
704
    return version;
2✔
705
  }
706

707
  private static boolean acceptVersion(VersionIdentifier version, GenericVersionRange allowedVersions, boolean requireStableVersion) {
708
    if (allowedVersions.isPattern() && !allowedVersions.contains(version)) {
3!
709
      return false;
×
710
    } else if (requireStableVersion && !version.isStable()) {
5!
711
      return false;
×
712
    }
713
    return true;
2✔
714
  }
715

716
  /**
717
   * @return the {@link MacOsHelper} instance.
718
   */
719
  protected MacOsHelper getMacOsHelper() {
720

721
    if (this.macOsHelper == null) {
3✔
722
      this.macOsHelper = new MacOsHelper(this.context);
7✔
723
    }
724
    return this.macOsHelper;
3✔
725
  }
726

727
  /**
728
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
729
   */
730
  public abstract VersionIdentifier getInstalledVersion();
731

732
  /**
733
   * @return {@code true} if this tool is installed, {@code false} otherwise.
734
   */
735
  public boolean isInstalled() {
736

737
    return getInstalledVersion() != null;
7✔
738
  }
739

740
  /**
741
   * @return the installed edition of this tool or {@code null} if not installed.
742
   */
743
  public abstract String getInstalledEdition();
744

745
  /**
746
   * Uninstalls the {@link #getName() tool}.
747
   */
748
  public abstract void uninstall();
749

750
  /**
751
   * @return the {@link ToolRepository}.
752
   */
753
  public ToolRepository getToolRepository() {
754

755
    return this.context.getDefaultToolRepository();
4✔
756
  }
757

758
  /**
759
   * List the available editions of this tool.
760
   */
761
  public void listEditions() {
762

763
    List<String> editions = getToolRepository().getSortedEditions(getName());
6✔
764
    for (String edition : editions) {
10✔
765
      this.context.info(edition);
4✔
766
    }
1✔
767
  }
1✔
768

769
  /**
770
   * List the available versions of this tool.
771
   */
772
  public void listVersions() {
773

774
    List<VersionIdentifier> versions = getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
775
    for (VersionIdentifier vi : versions) {
10✔
776
      this.context.info(vi.toString());
5✔
777
    }
1✔
778
  }
1✔
779

780
  /**
781
   * @return the {@link com.devonfw.tools.ide.tool.repository.DefaultToolRepository#getSortedVersions(String, String, ToolCommandlet) sorted versions} of this
782
   *     tool.
783
   */
784
  public List<VersionIdentifier> getVersions() {
785
    return getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
786
  }
787

788
  /**
789
   * Sets the tool version in the environment variable configuration file.
790
   *
791
   * @param version the version (pattern) to set.
792
   */
793
  public void setVersion(String version) {
794

795
    if ((version == null) || version.isBlank()) {
×
796
      throw new IllegalStateException("Version has to be specified!");
×
797
    }
798
    VersionIdentifier configuredVersion = VersionIdentifier.of(version);
×
799
    if (!configuredVersion.isPattern() && !configuredVersion.isValid()) {
×
800
      this.context.warning("Version {} seems to be invalid", version);
×
801
    }
802
    setVersion(configuredVersion, true);
×
803
  }
×
804

805
  /**
806
   * Sets the tool version in the environment variable configuration file.
807
   *
808
   * @param version the version to set. May also be a {@link VersionIdentifier#isPattern() version pattern}.
809
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
810
   */
811
  public void setVersion(VersionIdentifier version, boolean hint) {
812

813
    setVersion(version, hint, null);
5✔
814
  }
1✔
815

816
  /**
817
   * Sets the tool version in the environment variable configuration file.
818
   *
819
   * @param version the version to set. May also be a {@link VersionIdentifier#isPattern() version pattern}.
820
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
821
   * @param destination - the destination for the property to be set
822
   */
823
  public void setVersion(VersionIdentifier version, boolean hint, EnvironmentVariablesFiles destination) {
824

825
    String edition = getConfiguredEdition();
3✔
826
    ToolRepository toolRepository = getToolRepository();
3✔
827

828
    EnvironmentVariables variables = this.context.getVariables();
4✔
829
    if (destination == null) {
2✔
830
      //use default location
831
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
832
    }
833
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
834
    String name = EnvironmentVariables.getToolVersionVariable(this.tool);
4✔
835

836
    toolRepository.resolveVersion(this.tool, edition, version, this); // verify that the version actually exists
8✔
837
    settingsVariables.set(name, version.toString(), false);
7✔
838
    settingsVariables.save();
2✔
839
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
840
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
841
      this.context.warning("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name,
13✔
842
          declaringVariables.getSource());
2✔
843
    }
844
    if (hint) {
2✔
845
      this.context.info("To install that version call the following command:");
4✔
846
      this.context.info("ide install {}", this.tool);
11✔
847
    }
848
  }
1✔
849

850
  /**
851
   * Sets the tool edition in the environment variable configuration file.
852
   *
853
   * @param edition the edition to set.
854
   */
855
  public void setEdition(String edition) {
856

857
    setEdition(edition, true);
4✔
858
  }
1✔
859

860
  /**
861
   * Sets the tool edition in the environment variable configuration file.
862
   *
863
   * @param edition the edition to set
864
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
865
   */
866
  public void setEdition(String edition, boolean hint) {
867

868
    setEdition(edition, hint, null);
5✔
869
  }
1✔
870

871
  /**
872
   * Sets the tool edition in the environment variable configuration file.
873
   *
874
   * @param edition the edition to set
875
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
876
   * @param destination - the destination for the property to be set
877
   */
878
  public void setEdition(String edition, boolean hint, EnvironmentVariablesFiles destination) {
879

880
    if ((edition == null) || edition.isBlank()) {
5!
881
      throw new IllegalStateException("Edition has to be specified!");
×
882
    }
883

884
    if (destination == null) {
2✔
885
      //use default location
886
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
887
    }
888

889
    if (!getToolRepository().getSortedEditions(this.tool).contains(edition)) {
8✔
890
      this.context.warning("Edition {} seems to be invalid", edition);
10✔
891
    }
892
    EnvironmentVariables variables = this.context.getVariables();
4✔
893
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
894
    String name = EnvironmentVariables.getToolEditionVariable(this.tool);
4✔
895
    settingsVariables.set(name, edition, false);
6✔
896
    settingsVariables.save();
2✔
897

898
    this.context.info("{}={} has been set in {}", name, edition, settingsVariables.getSource());
19✔
899
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
900
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
901
      this.context.warning("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name,
13✔
902
          declaringVariables.getSource());
2✔
903
    }
904
    if (hint) {
2!
905
      this.context.info("To install that edition call the following command:");
4✔
906
      this.context.info("ide install {}", this.tool);
11✔
907
    }
908
  }
1✔
909

910
  /**
911
   * Runs the tool's help command to provide the user with usage information.
912
   */
913
  @Override
914
  public void printHelp(NlsBundle bundle) {
915

916
    super.printHelp(bundle);
3✔
917
    String toolHelpArgs = getToolHelpArguments();
3✔
918
    if (toolHelpArgs != null && getInstalledVersion() != null) {
5!
919
      ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.LOG_WARNING)
6✔
920
          .executable(Path.of(getBinaryName())).addArgs(toolHelpArgs);
13✔
921
      pc.run(ProcessMode.DEFAULT);
4✔
922
    }
923
  }
1✔
924

925
  /**
926
   * @return the tool's specific help command. Usually help, --help or -h. Return null if not applicable.
927
   */
928
  public String getToolHelpArguments() {
929

930
    return null;
×
931
  }
932

933
  /**
934
   * Creates a start script for the tool using the tool name.
935
   *
936
   * @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
937
   *     instead.
938
   * @param binary name of the binary to execute from the start script.
939
   */
940
  protected void createStartScript(Path targetDir, String binary) {
941

942
    createStartScript(targetDir, binary, false);
×
943
  }
×
944

945
  /**
946
   * Creates a start script for the tool using the tool name.
947
   *
948
   * @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
949
   *     instead.
950
   * @param binary name of the binary to execute from the start script.
951
   * @param background {@code true} to run the {@code binary} in background, {@code false} otherwise (foreground).
952
   */
953
  protected void createStartScript(Path targetDir, String binary, boolean background) {
954

955
    Path binFolder = targetDir.resolve("bin");
×
956
    if (!Files.exists(binFolder)) {
×
957
      if (this.context.getSystemInfo().isMac()) {
×
958
        MacOsHelper macOsHelper = getMacOsHelper();
×
959
        Path appDir = macOsHelper.findAppDir(targetDir);
×
960
        binFolder = macOsHelper.findLinkDir(appDir, binary);
×
961
      } else {
×
962
        binFolder = targetDir;
×
963
      }
964
      assert (Files.exists(binFolder));
×
965
    }
966
    Path bashFile = binFolder.resolve(getName());
×
967
    String bashFileContentStart = "#!/usr/bin/env bash\n\"$(dirname \"$0\")/";
×
968
    String bashFileContentEnd = "\" $@";
×
969
    if (background) {
×
970
      bashFileContentEnd += " &";
×
971
    }
972
    try {
973
      Files.writeString(bashFile, bashFileContentStart + binary + bashFileContentEnd);
×
974
    } catch (IOException e) {
×
975
      throw new RuntimeException(e);
×
976
    }
×
977
    assert (Files.exists(bashFile));
×
978
    context.getFileAccess().makeExecutable(bashFile);
×
979
  }
×
980

981
  @Override
982
  public void reset() {
983
    super.reset();
2✔
984
    this.executionDirectory = null;
3✔
985
  }
1✔
986

987
  /**
988
   * @param command the binary that will be searched in the PATH e.g. docker
989
   * @return true if the command is available to use
990
   */
991
  protected boolean isCommandAvailable(String command) {
992
    return this.context.getPath().hasBinaryOnPath(command);
×
993
  }
994

995
  /**
996
   * @param output the raw output string from executed command e.g. 'docker version'
997
   * @param pattern Regular Expression pattern that filters out the unnecessary texts.
998
   * @return version that has been processed.
999
   */
1000
  protected VersionIdentifier resolveVersionWithPattern(String output, Pattern pattern) {
1001
    Matcher matcher = pattern.matcher(output);
×
1002

1003
    if (matcher.find()) {
×
1004
      return VersionIdentifier.of(matcher.group(1));
×
1005
    } else {
1006
      return null;
×
1007
    }
1008
  }
1009

1010
  /**
1011
   * @param step the {@link Step} to get {@link Step#asSuccess() success logger} from. May be {@code null}.
1012
   * @return the {@link IdeSubLogger} from {@link Step#asSuccess()} or {@link IdeContext#success()} as fallback.
1013
   */
1014
  protected IdeSubLogger asSuccess(Step step) {
1015

1016
    if (step == null) {
2!
1017
      return this.context.success();
4✔
1018
    } else {
1019
      return step.asSuccess();
×
1020
    }
1021
  }
1022

1023

1024
  /**
1025
   * @param step the {@link Step} to get {@link Step#asError() error logger} from. May be {@code null}.
1026
   * @return the {@link IdeSubLogger} from {@link Step#asError()} or {@link IdeContext#error()} as fallback.
1027
   */
1028
  protected IdeSubLogger asError(Step step) {
1029

1030
    if (step == null) {
×
1031
      return this.context.error();
×
1032
    } else {
1033
      return step.asError();
×
1034
    }
1035
  }
1036
}
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