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

devonfw / IDEasy / 22315552991

23 Feb 2026 04:40PM UTC coverage: 70.563% (+0.09%) from 70.474%
22315552991

Pull #1714

github

web-flow
Merge 7d88da975 into 379acdc9d
Pull Request #1714: #404: #1713: advanced logging

4081 of 6382 branches covered (63.95%)

Branch coverage included in aggregate %.

10644 of 14486 relevant lines covered (73.48%)

3.09 hits per line

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

73.62
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 org.slf4j.Logger;
14
import org.slf4j.LoggerFactory;
15
import org.slf4j.event.Level;
16

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

44
/**
45
 * {@link Commandlet} for a tool integrated into the IDE.
46
 */
47
public abstract class ToolCommandlet extends Commandlet implements Tags {
48

49
  private static final Logger LOG = LoggerFactory.getLogger(ToolCommandlet.class);
4✔
50

51
  /** @see #getName() */
52
  protected final String tool;
53

54
  private final Set<Tag> tags;
55

56
  /** The commandline arguments to pass to the tool. */
57
  public final StringProperty arguments;
58

59
  private Path executionDirectory;
60

61
  private MacOsHelper macOsHelper;
62

63
  /**
64
   * The constructor.
65
   *
66
   * @param context the {@link IdeContext}.
67
   * @param tool the {@link #getName() tool name}.
68
   * @param tags the {@link #getTags() tags} classifying the tool. Should be created via {@link Set#of(Object) Set.of} method.
69
   */
70
  public ToolCommandlet(IdeContext context, String tool, Set<Tag> tags) {
71

72
    super(context);
3✔
73
    this.tool = tool;
3✔
74
    this.tags = tags;
3✔
75
    addKeyword(tool);
3✔
76
    this.arguments = new StringProperty("", false, true, "args");
9✔
77
    initProperties();
2✔
78
  }
1✔
79

80
  /**
81
   * Add initial Properties to the tool
82
   */
83
  protected void initProperties() {
84

85
    add(this.arguments);
5✔
86
  }
1✔
87

88
  /**
89
   * @return the name of the tool (e.g. "java", "mvn", "npm", "node").
90
   */
91
  @Override
92
  public final String getName() {
93

94
    return this.tool;
3✔
95
  }
96

97
  /**
98
   * @return the name of the binary executable for this tool.
99
   */
100
  protected String getBinaryName() {
101

102
    return this.tool;
3✔
103
  }
104

105
  /**
106
   * @return the {@link Path} to the installed {@link #getBinaryName() binary} or {@code null} if not found on the
107
   *     {@link com.devonfw.tools.ide.common.SystemPath}.
108
   */
109
  protected Path getBinaryExecutable() {
110

111
    Path binary = this.context.getPath().findBinaryPathByName(getBinaryName());
×
112
    if (binary.getParent() == null) {
×
113
      return null;
×
114
    }
115
    return binary;
×
116
  }
117

118
  @Override
119
  public final Set<Tag> getTags() {
120

121
    return this.tags;
3✔
122
  }
123

124
  /**
125
   * @return the execution directory where the tool will be executed. Will be {@code null} by default leading to execution in the users current working
126
   *     directory where IDEasy was called.
127
   * @see #setExecutionDirectory(Path)
128
   */
129
  public Path getExecutionDirectory() {
130
    return this.executionDirectory;
×
131
  }
132

133
  /**
134
   * @param executionDirectory the new value of {@link #getExecutionDirectory()}.
135
   */
136
  public void setExecutionDirectory(Path executionDirectory) {
137
    this.executionDirectory = executionDirectory;
×
138
  }
×
139

140
  /**
141
   * @return the {@link EnvironmentVariables#getToolVersion(String) tool version}.
142
   */
143
  public VersionIdentifier getConfiguredVersion() {
144

145
    return this.context.getVariables().getToolVersion(getName());
7✔
146
  }
147

148
  /**
149
   * @return the {@link EnvironmentVariables#getToolEdition(String) tool edition}.
150
   */
151
  public String getConfiguredEdition() {
152

153
    return this.context.getVariables().getToolEdition(getName());
7✔
154
  }
155

156
  /**
157
   * @return the {@link ToolEdition} with {@link #getName() tool} with its {@link #getConfiguredEdition() edition}.
158
   */
159
  protected final ToolEdition getToolWithConfiguredEdition() {
160

161
    return new ToolEdition(this.tool, getConfiguredEdition());
8✔
162
  }
163

164
  @Override
165
  protected void doRun() {
166

167
    runTool(this.arguments.asList());
6✔
168
  }
1✔
169

170
  /**
171
   * @param args the command-line arguments to run the tool.
172
   * @return the {@link ProcessResult result}.
173
   * @see ToolCommandlet#runTool(ProcessMode, GenericVersionRange, List)
174
   */
175
  public ProcessResult runTool(List<String> args) {
176

177
    return runTool(ProcessMode.DEFAULT, null, args);
6✔
178
  }
179

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

191
    return runTool(processMode, toolVersion, ProcessErrorHandling.THROW_CLI, args);
7✔
192
  }
193

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

206
    ProcessContext pc = this.context.newProcess().errorHandling(errorHandling);
6✔
207
    ToolInstallRequest request = new ToolInstallRequest(true);
5✔
208
    if (toolVersion != null) {
2!
209
      request.setRequested(new ToolEditionAndVersion(toolVersion));
×
210
    }
211
    request.setProcessContext(pc);
3✔
212
    return runTool(request, processMode, args);
6✔
213
  }
214

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

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

236
  /**
237
   * @param pc the {@link ProcessContext}.
238
   * @param processMode the {@link ProcessMode}. Should typically be {@link ProcessMode#DEFAULT} or {@link ProcessMode#BACKGROUND}.
239
   * @param args the command-line arguments to run the tool.
240
   * @return the {@link ProcessResult result}.
241
   */
242
  public ProcessResult runTool(ProcessContext pc, ProcessMode processMode, List<String> args) {
243

244
    if (this.executionDirectory != null) {
3!
245
      pc.directory(this.executionDirectory);
×
246
    }
247
    configureToolBinary(pc, processMode);
4✔
248
    configureToolArgs(pc, processMode, args);
5✔
249
    return pc.run(processMode);
4✔
250
  }
251

252
  /**
253
   * @param pc the {@link ProcessContext}.
254
   * @param processMode the {@link ProcessMode}.
255
   */
256
  protected void configureToolBinary(ProcessContext pc, ProcessMode processMode) {
257

258
    pc.executable(Path.of(getBinaryName()));
8✔
259
  }
1✔
260

261
  /**
262
   * @param pc the {@link ProcessContext}.
263
   * @param processMode the {@link ProcessMode}.
264
   * @param args the command-line arguments to {@link ProcessContext#addArgs(List) add}.
265
   */
266
  protected void configureToolArgs(ProcessContext pc, ProcessMode processMode, List<String> args) {
267

268
    pc.addArgs(args);
4✔
269
  }
1✔
270

271
  /**
272
   * Installs or updates the managed {@link #getName() tool}.
273
   *
274
   * @return the {@link ToolInstallation}.
275
   */
276
  public ToolInstallation install() {
277

278
    return install(true);
4✔
279
  }
280

281
  /**
282
   * Performs the installation of the {@link #getName() tool} managed by this {@link com.devonfw.tools.ide.commandlet.Commandlet}.
283
   *
284
   * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
285
   * @return the {@link ToolInstallation}.
286
   */
287
  public ToolInstallation install(boolean silent) {
288
    return install(new ToolInstallRequest(silent));
7✔
289
  }
290

291
  /**
292
   * Performs the installation (install, update, downgrade) of the {@link #getName() tool} managed by this {@link ToolCommandlet}.
293
   *
294
   * @param request the {@link ToolInstallRequest}.
295
   * @return the {@link ToolInstallation}.
296
   */
297
  public ToolInstallation install(ToolInstallRequest request) {
298

299
    completeRequest(request);
3✔
300
    if (request.isInstallLoop()) {
3!
301
      return toolAlreadyInstalled(request);
×
302
    }
303
    return doInstall(request);
4✔
304
  }
305

306
  /**
307
   * Performs the installation (install, update, downgrade) of the {@link #getName() tool} managed by this {@link ToolCommandlet}.
308
   *
309
   * @param request the {@link ToolInstallRequest}.
310
   * @return the {@link ToolInstallation}.
311
   */
312
  protected abstract ToolInstallation doInstall(ToolInstallRequest request);
313

314
  /**
315
   * @param request the {@link ToolInstallRequest} to complete (fill values that are currently {@code null}).
316
   */
317
  protected void completeRequest(ToolInstallRequest request) {
318

319
    completeRequestInstalled(request);
3✔
320
    completeRequestRequested(request); // depends on completeRequestInstalled
3✔
321
    completeRequestProcessContext(request);
3✔
322
    completeRequestToolPath(request);
3✔
323
  }
1✔
324

325
  private void completeRequestProcessContext(ToolInstallRequest request) {
326
    if (request.getProcessContext() == null) {
3✔
327
      ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_CLI);
6✔
328
      request.setProcessContext(pc);
3✔
329
    }
330
  }
1✔
331

332
  private void completeRequestInstalled(ToolInstallRequest request) {
333

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

364
  private void completeRequestRequested(ToolInstallRequest request) {
365

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

402
  private void completeRequestToolPath(ToolInstallRequest request) {
403

404
    Path toolPath = request.getToolPath();
3✔
405
    if (toolPath == null) {
2✔
406
      toolPath = getToolPath();
3✔
407
      request.setToolPath(toolPath);
3✔
408
    }
409
  }
1✔
410

411
  /**
412
   * @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
413
   *     determined by the installer.
414
   */
415
  public Path getToolPath() {
416
    return null;
×
417
  }
418

419
  /**
420
   * This method is called after a tool was requested to be installed or updated.
421
   *
422
   * @param request {@code true} the {@link ToolInstallRequest}.
423
   */
424
  protected void postInstall(ToolInstallRequest request) {
425

426
    if (!request.isAlreadyInstalled()) {
3✔
427
      postInstallOnNewInstallation(request);
3✔
428
    }
429
  }
1✔
430

431
  /**
432
   * This method is called after a tool was requested to be installed or updated and a new installation was performed.
433
   *
434
   * @param request {@code true} the {@link ToolInstallRequest}.
435
   */
436
  protected void postInstallOnNewInstallation(ToolInstallRequest request) {
437

438
    // nothing to do by default
439
  }
1✔
440

441
  /**
442
   * @param edition the {@link #getInstalledEdition() edition}.
443
   * @param version the {@link #getInstalledVersion() version}.
444
   * @return the {@link Path} where this tool is installed (physically) or {@code null} if not available.
445
   */
446
  protected abstract Path getInstallationPath(String edition, VersionIdentifier version);
447

448
  /**
449
   * @param request the {@link ToolInstallRequest}.
450
   * @return the existing {@link ToolInstallation}.
451
   */
452
  protected ToolInstallation createExistingToolInstallation(ToolInstallRequest request) {
453

454
    ToolEditionAndVersion installed = request.getInstalled();
3✔
455

456
    String edition = this.tool;
3✔
457
    VersionIdentifier resolvedVersion = VersionIdentifier.LATEST;
2✔
458

459
    if (installed != null) {
2!
460
      if (installed.getEdition() != null) {
3!
461
        edition = installed.getEdition().edition();
4✔
462
      }
463
      if (installed.getResolvedVersion() != null) {
3!
464
        resolvedVersion = installed.getResolvedVersion();
3✔
465
      }
466
    }
467

468
    return createExistingToolInstallation(edition, resolvedVersion, request.getProcessContext(),
8✔
469
        request.isAdditionalInstallation());
1✔
470
  }
471

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

483
    Path installationPath = getInstallationPath(edition, installedVersion);
5✔
484
    return createToolInstallation(installationPath, installedVersion, false, environmentContext, extraInstallation);
8✔
485
  }
486

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

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

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

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

535
  /**
536
   * Called if the tool {@link ToolInstallRequest#isAlreadyInstalled() is already installed in the correct edition and version} so we can skip the
537
   * installation.
538
   *
539
   * @param request the {@link ToolInstallRequest}.
540
   * @return the {@link ToolInstallation}.
541
   */
542
  protected ToolInstallation toolAlreadyInstalled(ToolInstallRequest request) {
543

544
    logToolAlreadyInstalled(request);
3✔
545
    cveCheck(request);
4✔
546
    postInstall(request);
3✔
547
    return createExistingToolInstallation(request);
4✔
548
  }
549

550
  /**
551
   * Log that the tool is already installed.
552
   *
553
   * @param request the {@link ToolInstallRequest}.
554
   */
555
  protected void logToolAlreadyInstalled(ToolInstallRequest request) {
556
    Level level;
557
    if (request.isSilent()) {
3✔
558
      level = Level.DEBUG;
3✔
559
    } else {
560
      level = Level.INFO;
2✔
561
    }
562
    ToolEditionAndVersion installed = request.getInstalled();
3✔
563
    LOG.atLevel(level).log("Version {} of tool {} is already installed", installed.getVersion(), installed.getEdition());
9✔
564
  }
1✔
565

566
  /**
567
   * Method to get the home path of the given {@link ToolInstallation}.
568
   *
569
   * @param toolInstallation the {@link ToolInstallation}.
570
   * @return the Path to the home of the tool
571
   */
572
  protected Path getToolHomePath(ToolInstallation toolInstallation) {
573
    return toolInstallation.linkDir();
3✔
574
  }
575

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

587
    String pathVariable = EnvironmentVariables.getToolVariablePrefix(this.tool) + "_HOME";
5✔
588
    Path toolHomePath = getToolHomePath(toolInstallation);
4✔
589
    if (toolHomePath != null) {
2!
590
      environmentContext.withEnvVar(pathVariable, toolHomePath.toString());
6✔
591
    }
592
    if (additionalInstallation) {
2✔
593
      environmentContext.withPathEntry(toolInstallation.binDir());
5✔
594
    }
595
  }
1✔
596

597
  /**
598
   * @return {@code true} to extract (unpack) the downloaded binary file, {@code false} otherwise.
599
   */
600
  protected boolean isExtract() {
601

602
    return true;
2✔
603
  }
604

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

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

650
      if (Objects.equals(version, resolvedVersion)) {
4✔
651
        continue; // Skip the entire iteration for resolvedVersion
1✔
652
      }
653

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

712
  private static boolean acceptVersion(VersionIdentifier version, GenericVersionRange allowedVersions, boolean requireStableVersion) {
713
    if (allowedVersions.isPattern() && !allowedVersions.contains(version)) {
3!
714
      return false;
×
715
    } else if (requireStableVersion && !version.isStable()) {
5!
716
      return false;
×
717
    }
718
    return true;
2✔
719
  }
720

721
  /**
722
   * @return the {@link MacOsHelper} instance.
723
   */
724
  protected MacOsHelper getMacOsHelper() {
725

726
    if (this.macOsHelper == null) {
3✔
727
      this.macOsHelper = new MacOsHelper(this.context);
7✔
728
    }
729
    return this.macOsHelper;
3✔
730
  }
731

732
  /**
733
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
734
   */
735
  public abstract VersionIdentifier getInstalledVersion();
736

737
  /**
738
   * @return {@code true} if this tool is installed, {@code false} otherwise.
739
   */
740
  public boolean isInstalled() {
741

742
    return getInstalledVersion() != null;
7✔
743
  }
744

745
  /**
746
   * @return the installed edition of this tool or {@code null} if not installed.
747
   */
748
  public abstract String getInstalledEdition();
749

750
  /**
751
   * Uninstalls the {@link #getName() tool}.
752
   */
753
  public abstract void uninstall();
754

755
  /**
756
   * @return the {@link ToolRepository}.
757
   */
758
  public ToolRepository getToolRepository() {
759

760
    return this.context.getDefaultToolRepository();
4✔
761
  }
762

763
  /**
764
   * List the available editions of this tool.
765
   */
766
  public void listEditions() {
767

768
    List<String> editions = getToolRepository().getSortedEditions(getName());
6✔
769
    for (String edition : editions) {
10✔
770
      LOG.info(edition);
3✔
771
    }
1✔
772
  }
1✔
773

774
  /**
775
   * List the available versions of this tool.
776
   */
777
  public void listVersions() {
778

779
    List<VersionIdentifier> versions = getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
780
    for (VersionIdentifier vi : versions) {
10✔
781
      LOG.info(vi.toString());
4✔
782
    }
1✔
783
  }
1✔
784

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

793
  /**
794
   * Sets the tool version in the environment variable configuration file.
795
   *
796
   * @param version the version (pattern) to set.
797
   */
798
  public void setVersion(String version) {
799

800
    if ((version == null) || version.isBlank()) {
×
801
      throw new IllegalStateException("Version has to be specified!");
×
802
    }
803
    VersionIdentifier configuredVersion = VersionIdentifier.of(version);
×
804
    if (!configuredVersion.isPattern() && !configuredVersion.isValid()) {
×
805
      LOG.warn("Version {} seems to be invalid", version);
×
806
    }
807
    setVersion(configuredVersion, true);
×
808
  }
×
809

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

818
    setVersion(version, hint, null);
5✔
819
  }
1✔
820

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

830
    String edition = getConfiguredEdition();
3✔
831
    ToolRepository toolRepository = getToolRepository();
3✔
832

833
    EnvironmentVariables variables = this.context.getVariables();
4✔
834
    if (destination == null) {
2✔
835
      //use default location
836
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
837
    }
838
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
839
    String name = EnvironmentVariables.getToolVersionVariable(this.tool);
4✔
840

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

855
  /**
856
   * Sets the tool edition in the environment variable configuration file.
857
   *
858
   * @param edition the edition to set.
859
   */
860
  public void setEdition(String edition) {
861

862
    setEdition(edition, true);
4✔
863
  }
1✔
864

865
  /**
866
   * Sets the tool edition in the environment variable configuration file.
867
   *
868
   * @param edition the edition to set
869
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
870
   */
871
  public void setEdition(String edition, boolean hint) {
872

873
    setEdition(edition, hint, null);
5✔
874
  }
1✔
875

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

885
    if ((edition == null) || edition.isBlank()) {
5!
886
      throw new IllegalStateException("Edition has to be specified!");
×
887
    }
888

889
    if (destination == null) {
2✔
890
      //use default location
891
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
892
    }
893

894
    if (!getToolRepository().getSortedEditions(this.tool).contains(edition)) {
8✔
895
      LOG.warn("Edition {} seems to be invalid", edition);
4✔
896
    }
897
    EnvironmentVariables variables = this.context.getVariables();
4✔
898
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
899
    String name = EnvironmentVariables.getToolEditionVariable(this.tool);
4✔
900
    settingsVariables.set(name, edition, false);
6✔
901
    settingsVariables.save();
2✔
902

903
    LOG.info("{}={} has been set in {}", name, edition, settingsVariables.getSource());
18✔
904
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
905
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
906
      LOG.warn("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name,
5✔
907
          declaringVariables.getSource());
1✔
908
    }
909
    if (hint) {
2!
910
      LOG.info("To install that edition call the following command:");
3✔
911
      LOG.info("ide install {}", this.tool);
5✔
912
    }
913
  }
1✔
914

915
  /**
916
   * Runs the tool's help command to provide the user with usage information.
917
   */
918
  @Override
919
  public void printHelp(NlsBundle bundle) {
920

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

930
  /**
931
   * @return the tool's specific help command. Usually help, --help or -h. Return null if not applicable.
932
   */
933
  public String getToolHelpArguments() {
934

935
    return null;
×
936
  }
937

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

947
    createStartScript(targetDir, binary, false);
×
948
  }
×
949

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

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

986
  @Override
987
  public void reset() {
988
    super.reset();
2✔
989
    this.executionDirectory = null;
3✔
990
  }
1✔
991

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

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

1008
    if (matcher.find()) {
×
1009
      return VersionIdentifier.of(matcher.group(1));
×
1010
    } else {
1011
      return null;
×
1012
    }
1013
  }
1014

1015
  /**
1016
   * @deprecated directly log success message and then report success on step if not null.
1017
   */
1018
  @Deprecated
1019
  protected void success(Step step, String message, Object... args) {
1020

1021
    if (step == null) {
×
1022
      IdeLogLevel.SUCCESS.log(LOG, message, args);
×
1023
    } else {
1024
      step.success(message, args);
×
1025
    }
1026
  }
×
1027

1028
}
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