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

devonfw / IDEasy / 21013659204

14 Jan 2026 11:29PM UTC coverage: 70.365% (+0.5%) from 69.904%
21013659204

Pull #1675

github

web-flow
Merge 7a3aa598b into fcadaae82
Pull Request #1675: #1298: support ide-extra-tools.json #1658: prevent Jackson reflection

4015 of 6292 branches covered (63.81%)

Branch coverage included in aggregate %.

10440 of 14251 relevant lines covered (73.26%)

3.17 hits per line

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

74.48
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
  @Override
100
  public final Set<Tag> getTags() {
101

102
    return this.tags;
3✔
103
  }
104

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

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

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

126
    return this.context.getVariables().getToolVersion(getName());
7✔
127
  }
128

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

134
    return this.context.getVariables().getToolEdition(getName());
7✔
135
  }
136

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

142
    return new ToolEdition(this.tool, getConfiguredEdition());
8✔
143
  }
144

145
  @Override
146
  public void run() {
147

148
    runTool(this.arguments.asList());
6✔
149
  }
1✔
150

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

158
    return runTool(ProcessMode.DEFAULT, null, args);
6✔
159
  }
160

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

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

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

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

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

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

217
  /**
218
   * @param pc the {@link ProcessContext}.
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(ProcessContext pc, ProcessMode processMode, List<String> args) {
224

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

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

239
    pc.executable(Path.of(getBinaryName()));
8✔
240
  }
1✔
241

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

249
    pc.addArgs(args);
4✔
250
  }
1✔
251

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

259
    return install(true);
4✔
260
  }
261

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

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

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

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

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

300
    completeRequestInstalled(request);
3✔
301
    completeRequestRequested(request); // depends on completeRequestInstalled
3✔
302
    completeRequestProcessContext(request);
3✔
303
    completeRequestToolPath(request);
3✔
304
  }
1✔
305

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

313
  private void completeRequestInstalled(ToolInstallRequest request) {
314

315
    ToolEditionAndVersion installedToolVersion = request.getInstalled();
3✔
316
    if (installedToolVersion == null) {
2✔
317
      installedToolVersion = new ToolEditionAndVersion((GenericVersionRange) null);
6✔
318
      request.setInstalled(installedToolVersion);
3✔
319
    }
320
    Path toolPath = request.getToolPath();
3✔
321
    if (installedToolVersion.getVersion() == null) {
3✔
322
      VersionIdentifier installedVersion;
323
      if ((toolPath != null) && (this instanceof LocalToolCommandlet ltc)) {
10!
324
        installedVersion = ltc.getInstalledVersion(toolPath);
5✔
325
      } else {
326
        installedVersion = getInstalledVersion();
3✔
327
      }
328
      if (installedVersion == null) {
2✔
329
        return;
1✔
330
      }
331
      installedToolVersion.setVersion(installedVersion);
3✔
332
    }
333
    if (installedToolVersion.getEdition() == null) {
3✔
334
      String installedEdition;
335
      if ((toolPath != null) && (this instanceof LocalToolCommandlet ltc)) {
2!
336
        installedEdition = ltc.getInstalledEdition(toolPath);
×
337
      } else {
338
        installedEdition = getInstalledEdition();
3✔
339
      }
340
      installedToolVersion.setEdition(new ToolEdition(this.tool, installedEdition));
8✔
341
    }
342
    assert installedToolVersion.getResolvedVersion() != null;
4!
343
  }
1✔
344

345
  private void completeRequestRequested(ToolInstallRequest request) {
346

347
    ToolEdition edition;
348
    ToolEditionAndVersion requested = request.getRequested();
3✔
349
    if (requested == null) {
2✔
350
      edition = new ToolEdition(this.tool, getConfiguredEdition());
8✔
351
      requested = new ToolEditionAndVersion(edition);
5✔
352
      request.setRequested(requested);
4✔
353
    } else {
354
      edition = requested.getEdition();
3✔
355
      if (edition == null) {
2✔
356
        edition = new ToolEdition(this.tool, getConfiguredEdition());
8✔
357
        requested.setEdition(edition);
3✔
358
      }
359
    }
360
    GenericVersionRange version = requested.getVersion();
3✔
361
    if (version == null) {
2✔
362
      version = getConfiguredVersion();
3✔
363
      requested.setVersion(version);
3✔
364
    }
365
    VersionIdentifier resolvedVersion = requested.getResolvedVersion();
3✔
366
    if (resolvedVersion == null) {
2✔
367
      if (this.context.isSkipUpdatesMode()) {
4✔
368
        ToolEditionAndVersion installed = request.getInstalled();
3✔
369
        if (installed != null) {
2!
370
          VersionIdentifier installedVersion = installed.getResolvedVersion();
3✔
371
          if (version.contains(installedVersion)) {
4✔
372
            resolvedVersion = installedVersion;
2✔
373
          }
374
        }
375
      }
376
      if (resolvedVersion == null) {
2✔
377
        resolvedVersion = getToolRepository().resolveVersion(this.tool, edition.edition(), version, this);
10✔
378
      }
379
      requested.setResolvedVersion(resolvedVersion);
3✔
380
    }
381
  }
1✔
382

383
  private void completeRequestToolPath(ToolInstallRequest request) {
384

385
    Path toolPath = request.getToolPath();
3✔
386
    if (toolPath == null) {
2✔
387
      toolPath = getToolPath();
3✔
388
      request.setToolPath(toolPath);
3✔
389
    }
390
  }
1✔
391

392
  /**
393
   * @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
394
   *     determined by the installer.
395
   */
396
  public Path getToolPath() {
397
    return null;
×
398
  }
399

400
  /**
401
   * This method is called after a tool was requested to be installed or updated.
402
   *
403
   * @param request {@code true} the {@link ToolInstallRequest}.
404
   */
405
  protected void postInstall(ToolInstallRequest request) {
406

407
    if (!request.isAlreadyInstalled()) {
3✔
408
      postInstallOnNewInstallation(request);
3✔
409
    }
410
  }
1✔
411

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

419
    // nothing to do by default
420
  }
1✔
421

422
  /**
423
   * @param edition the {@link #getInstalledEdition() edition}.
424
   * @param version the {@link #getInstalledVersion() version}.
425
   * @return the {@link Path} where this tool is installed (physically) or {@code null} if not available.
426
   */
427
  protected abstract Path getInstallationPath(String edition, VersionIdentifier version);
428

429
  /**
430
   * @param request the {@link ToolInstallRequest}.
431
   * @return the existing {@link ToolInstallation}.
432
   */
433
  protected ToolInstallation createExistingToolInstallation(ToolInstallRequest request) {
434

435
    ToolEditionAndVersion installed = request.getInstalled();
3✔
436

437
    String edition = this.tool;
3✔
438
    VersionIdentifier resolvedVersion = VersionIdentifier.LATEST;
2✔
439

440
    if (installed != null) {
2!
441
      if (installed.getEdition() != null) {
3!
442
        edition = installed.getEdition().edition();
4✔
443
      }
444
      if (installed.getResolvedVersion() != null) {
3!
445
        resolvedVersion = installed.getResolvedVersion();
3✔
446
      }
447
    }
448

449
    return createExistingToolInstallation(edition, resolvedVersion, request.getProcessContext(),
8✔
450
        request.isAdditionalInstallation());
1✔
451
  }
452

453
  /**
454
   * @param edition the {@link #getConfiguredEdition() edition}.
455
   * @param installedVersion the {@link #getConfiguredVersion() version}.
456
   * @param environmentContext the {@link EnvironmentContext}.
457
   * @param extraInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
458
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
459
   * @return the {@link ToolInstallation}.
460
   */
461
  protected ToolInstallation createExistingToolInstallation(String edition, VersionIdentifier installedVersion, EnvironmentContext environmentContext,
462
      boolean extraInstallation) {
463

464
    Path installationPath = getInstallationPath(edition, installedVersion);
5✔
465
    return createToolInstallation(installationPath, installedVersion, false, environmentContext, extraInstallation);
8✔
466
  }
467

468
  /**
469
   * @param rootDir the {@link ToolInstallation#rootDir() top-level installation directory}.
470
   * @param version the installed {@link VersionIdentifier}.
471
   * @param newInstallation {@link ToolInstallation#newInstallation() new installation} flag.
472
   * @param environmentContext the {@link EnvironmentContext}.
473
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
474
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
475
   * @return the {@link ToolInstallation}.
476
   */
477
  protected ToolInstallation createToolInstallation(Path rootDir, VersionIdentifier version, boolean newInstallation,
478
      EnvironmentContext environmentContext, boolean additionalInstallation) {
479

480
    Path linkDir = rootDir;
2✔
481
    Path binDir = rootDir;
2✔
482
    if (rootDir != null) {
2!
483
      // on MacOS applications have a very strange structure - see JavaDoc of findLinkDir and ToolInstallation.linkDir for details.
484
      linkDir = getMacOsHelper().findLinkDir(rootDir, getBinaryName());
7✔
485
      binDir = this.context.getFileAccess().getBinPath(linkDir);
6✔
486
    }
487
    return createToolInstallation(rootDir, linkDir, binDir, version, newInstallation, environmentContext, additionalInstallation);
10✔
488
  }
489

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

504
    if (linkDir != rootDir) {
3✔
505
      assert (!linkDir.equals(rootDir));
5!
506
      Path toolVersionFile = rootDir.resolve(IdeContext.FILE_SOFTWARE_VERSION);
4✔
507
      if (Files.exists(toolVersionFile)) {
5!
508
        this.context.getFileAccess().copy(toolVersionFile, linkDir, FileCopyMode.COPY_FILE_OVERRIDE);
7✔
509
      }
510
    }
511
    ToolInstallation toolInstallation = new ToolInstallation(rootDir, linkDir, binDir, version, newInstallation);
9✔
512
    setEnvironment(environmentContext, toolInstallation, additionalInstallation);
5✔
513
    return toolInstallation;
2✔
514
  }
515

516
  /**
517
   * Called if the tool {@link ToolInstallRequest#isAlreadyInstalled() is already installed in the correct edition and version} so we can skip the
518
   * installation.
519
   *
520
   * @param request the {@link ToolInstallRequest}.
521
   * @return the {@link ToolInstallation}.
522
   */
523
  protected ToolInstallation toolAlreadyInstalled(ToolInstallRequest request) {
524

525
    logToolAlreadyInstalled(request);
3✔
526
    cveCheck(request);
4✔
527
    postInstall(request);
3✔
528
    return createExistingToolInstallation(request);
4✔
529
  }
530

531
  /**
532
   * Log that the tool is already installed.
533
   *
534
   * @param request the {@link ToolInstallRequest}.
535
   */
536
  protected void logToolAlreadyInstalled(ToolInstallRequest request) {
537
    IdeSubLogger logger;
538
    if (request.isSilent()) {
3✔
539
      logger = this.context.debug();
5✔
540
    } else {
541
      logger = this.context.info();
4✔
542
    }
543
    ToolEditionAndVersion installed = request.getInstalled();
3✔
544
    logger.log("Version {} of tool {} is already installed", installed.getVersion(), installed.getEdition());
16✔
545
  }
1✔
546

547
  /**
548
   * Method to get the home path of the given {@link ToolInstallation}.
549
   *
550
   * @param toolInstallation the {@link ToolInstallation}.
551
   * @return the Path to the home of the tool
552
   */
553
  protected Path getToolHomePath(ToolInstallation toolInstallation) {
554
    return toolInstallation.linkDir();
3✔
555
  }
556

557
  /**
558
   * Method to set environment variables for the process context.
559
   *
560
   * @param environmentContext the {@link EnvironmentContext} where to {@link EnvironmentContext#withEnvVar(String, String) set environment variables} for
561
   *     this tool.
562
   * @param toolInstallation the {@link ToolInstallation}.
563
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
564
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
565
   */
566
  public void setEnvironment(EnvironmentContext environmentContext, ToolInstallation toolInstallation, boolean additionalInstallation) {
567

568
    String pathVariable = EnvironmentVariables.getToolVariablePrefix(this.tool) + "_HOME";
5✔
569
    Path toolHomePath = getToolHomePath(toolInstallation);
4✔
570
    if (toolHomePath != null) {
2!
571
      environmentContext.withEnvVar(pathVariable, toolHomePath.toString());
6✔
572
    }
573
    if (additionalInstallation) {
2✔
574
      environmentContext.withPathEntry(toolInstallation.binDir());
5✔
575
    }
576
  }
1✔
577

578
  /**
579
   * @return {@code true} to extract (unpack) the downloaded binary file, {@code false} otherwise.
580
   */
581
  protected boolean isExtract() {
582

583
    return true;
2✔
584
  }
585

586
  /**
587
   * 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
588
   * something better, we will suggest this to the user and ask him to make his choice.
589
   *
590
   * @param request the {@link ToolInstallRequest}.
591
   * @return the {@link VersionIdentifier} to install. The will may be asked (unless {@code skipSuggestions} is {@code true}) and might choose a different
592
   *     version than the originally requested one.
593
   */
594
  protected VersionIdentifier cveCheck(ToolInstallRequest request) {
595

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

631
      if (Objects.equals(version, resolvedVersion)) {
4✔
632
        continue; // Skip the entire iteration for resolvedVersion
1✔
633
      }
634

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

694
  private static boolean acceptVersion(VersionIdentifier version, GenericVersionRange allowedVersions, boolean requireStableVersion) {
695
    if (allowedVersions.isPattern() && !allowedVersions.contains(version)) {
3!
696
      return false;
×
697
    } else if (requireStableVersion && !version.isStable()) {
5!
698
      return false;
×
699
    }
700
    return true;
2✔
701
  }
702

703
  /**
704
   * @return the {@link MacOsHelper} instance.
705
   */
706
  protected MacOsHelper getMacOsHelper() {
707

708
    if (this.macOsHelper == null) {
3✔
709
      this.macOsHelper = new MacOsHelper(this.context);
7✔
710
    }
711
    return this.macOsHelper;
3✔
712
  }
713

714
  /**
715
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
716
   */
717
  public abstract VersionIdentifier getInstalledVersion();
718

719
  /**
720
   * @return {@code true} if this tool is installed, {@code false} otherwise.
721
   */
722
  public boolean isInstalled() {
723

724
    return getInstalledVersion() != null;
7✔
725
  }
726

727
  /**
728
   * @return the installed edition of this tool or {@code null} if not installed.
729
   */
730
  public abstract String getInstalledEdition();
731

732
  /**
733
   * Uninstalls the {@link #getName() tool}.
734
   */
735
  public abstract void uninstall();
736

737
  /**
738
   * @return the {@link ToolRepository}.
739
   */
740
  public ToolRepository getToolRepository() {
741

742
    return this.context.getDefaultToolRepository();
4✔
743
  }
744

745
  /**
746
   * List the available editions of this tool.
747
   */
748
  public void listEditions() {
749

750
    List<String> editions = getToolRepository().getSortedEditions(getName());
6✔
751
    for (String edition : editions) {
10✔
752
      this.context.info(edition);
4✔
753
    }
1✔
754
  }
1✔
755

756
  /**
757
   * List the available versions of this tool.
758
   */
759
  public void listVersions() {
760

761
    List<VersionIdentifier> versions = getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
762
    for (VersionIdentifier vi : versions) {
10✔
763
      this.context.info(vi.toString());
5✔
764
    }
1✔
765
  }
1✔
766

767
  /**
768
   * @return the {@link com.devonfw.tools.ide.tool.repository.DefaultToolRepository#getSortedVersions(String, String, ToolCommandlet) sorted versions} of this
769
   *     tool.
770
   */
771
  public List<VersionIdentifier> getVersions() {
772
    return getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
773
  }
774

775
  /**
776
   * Sets the tool version in the environment variable configuration file.
777
   *
778
   * @param version the version (pattern) to set.
779
   */
780
  public void setVersion(String version) {
781

782
    if ((version == null) || version.isBlank()) {
×
783
      throw new IllegalStateException("Version has to be specified!");
×
784
    }
785
    VersionIdentifier configuredVersion = VersionIdentifier.of(version);
×
786
    if (!configuredVersion.isPattern() && !configuredVersion.isValid()) {
×
787
      this.context.warning("Version {} seems to be invalid", version);
×
788
    }
789
    setVersion(configuredVersion, true);
×
790
  }
×
791

792
  /**
793
   * Sets the tool version in the environment variable configuration file.
794
   *
795
   * @param version the version to set. May also be a {@link VersionIdentifier#isPattern() version pattern}.
796
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
797
   */
798
  public void setVersion(VersionIdentifier version, boolean hint) {
799

800
    setVersion(version, hint, null);
5✔
801
  }
1✔
802

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

812
    String edition = getConfiguredEdition();
3✔
813
    ToolRepository toolRepository = getToolRepository();
3✔
814

815
    EnvironmentVariables variables = this.context.getVariables();
4✔
816
    if (destination == null) {
2✔
817
      //use default location
818
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
819
    }
820
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
821
    String name = EnvironmentVariables.getToolVersionVariable(this.tool);
4✔
822

823
    toolRepository.resolveVersion(this.tool, edition, version, this); // verify that the version actually exists
8✔
824
    settingsVariables.set(name, version.toString(), false);
7✔
825
    settingsVariables.save();
2✔
826
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
827
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
828
      this.context.warning("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name,
13✔
829
          declaringVariables.getSource());
2✔
830
    }
831
    if (hint) {
2✔
832
      this.context.info("To install that version call the following command:");
4✔
833
      this.context.info("ide install {}", this.tool);
11✔
834
    }
835
  }
1✔
836

837
  /**
838
   * Sets the tool edition in the environment variable configuration file.
839
   *
840
   * @param edition the edition to set.
841
   */
842
  public void setEdition(String edition) {
843

844
    setEdition(edition, true);
4✔
845
  }
1✔
846

847
  /**
848
   * Sets the tool edition in the environment variable configuration file.
849
   *
850
   * @param edition the edition to set
851
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
852
   */
853
  public void setEdition(String edition, boolean hint) {
854

855
    setEdition(edition, hint, null);
5✔
856
  }
1✔
857

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

867
    if ((edition == null) || edition.isBlank()) {
5!
868
      throw new IllegalStateException("Edition has to be specified!");
×
869
    }
870

871
    if (destination == null) {
2✔
872
      //use default location
873
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
874
    }
875

876
    if (!getToolRepository().getSortedEditions(this.tool).contains(edition)) {
8✔
877
      this.context.warning("Edition {} seems to be invalid", edition);
10✔
878
    }
879
    EnvironmentVariables variables = this.context.getVariables();
4✔
880
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
881
    String name = EnvironmentVariables.getToolEditionVariable(this.tool);
4✔
882
    settingsVariables.set(name, edition, false);
6✔
883
    settingsVariables.save();
2✔
884

885
    this.context.info("{}={} has been set in {}", name, edition, settingsVariables.getSource());
19✔
886
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
887
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
888
      this.context.warning("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name,
13✔
889
          declaringVariables.getSource());
2✔
890
    }
891
    if (hint) {
2!
892
      this.context.info("To install that edition call the following command:");
4✔
893
      this.context.info("ide install {}", this.tool);
11✔
894
    }
895
  }
1✔
896

897
  /**
898
   * Runs the tool's help command to provide the user with usage information.
899
   */
900
  @Override
901
  public void printHelp(NlsBundle bundle) {
902

903
    super.printHelp(bundle);
3✔
904
    String toolHelpArgs = getToolHelpArguments();
3✔
905
    if (toolHelpArgs != null && getInstalledVersion() != null) {
5!
906
      ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.LOG_WARNING)
6✔
907
          .executable(Path.of(getBinaryName())).addArgs(toolHelpArgs);
13✔
908
      pc.run(ProcessMode.DEFAULT);
4✔
909
    }
910
  }
1✔
911

912
  /**
913
   * @return the tool's specific help command. Usually help, --help or -h. Return null if not applicable.
914
   */
915
  public String getToolHelpArguments() {
916

917
    return null;
×
918
  }
919

920
  /**
921
   * Creates a start script for the tool using the tool name.
922
   *
923
   * @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
924
   *     instead.
925
   * @param binary name of the binary to execute from the start script.
926
   */
927
  protected void createStartScript(Path targetDir, String binary) {
928

929
    createStartScript(targetDir, binary, false);
×
930
  }
×
931

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

942
    Path binFolder = targetDir.resolve("bin");
×
943
    if (!Files.exists(binFolder)) {
×
944
      if (this.context.getSystemInfo().isMac()) {
×
945
        MacOsHelper macOsHelper = getMacOsHelper();
×
946
        Path appDir = macOsHelper.findAppDir(targetDir);
×
947
        binFolder = macOsHelper.findLinkDir(appDir, binary);
×
948
      } else {
×
949
        binFolder = targetDir;
×
950
      }
951
      assert (Files.exists(binFolder));
×
952
    }
953
    Path bashFile = binFolder.resolve(getName());
×
954
    String bashFileContentStart = "#!/usr/bin/env bash\n\"$(dirname \"$0\")/";
×
955
    String bashFileContentEnd = "\" $@";
×
956
    if (background) {
×
957
      bashFileContentEnd += " &";
×
958
    }
959
    try {
960
      Files.writeString(bashFile, bashFileContentStart + binary + bashFileContentEnd);
×
961
    } catch (IOException e) {
×
962
      throw new RuntimeException(e);
×
963
    }
×
964
    assert (Files.exists(bashFile));
×
965
    context.getFileAccess().makeExecutable(bashFile);
×
966
  }
×
967

968
  @Override
969
  public void reset() {
970
    super.reset();
2✔
971
    this.executionDirectory = null;
3✔
972
  }
1✔
973

974
  /**
975
   * @param command the binary that will be searched in the PATH e.g. docker
976
   * @return true if the command is available to use
977
   */
978
  protected boolean isCommandAvailable(String command) {
979
    return this.context.getPath().hasBinaryOnPath(command);
×
980
  }
981

982
  /**
983
   * @param output the raw output string from executed command e.g. 'docker version'
984
   * @param pattern Regular Expression pattern that filters out the unnecessary texts.
985
   * @return version that has been processed.
986
   */
987
  protected VersionIdentifier resolveVersionWithPattern(String output, Pattern pattern) {
988
    Matcher matcher = pattern.matcher(output);
×
989

990
    if (matcher.find()) {
×
991
      return VersionIdentifier.of(matcher.group(1));
×
992
    } else {
993
      return null;
×
994
    }
995
  }
996

997
  /**
998
   * @param step the {@link Step} to get {@link Step#asSuccess() success logger} from. May be {@code null}.
999
   * @return the {@link IdeSubLogger} from {@link Step#asSuccess()} or {@link IdeContext#success()} as fallback.
1000
   */
1001
  protected IdeSubLogger asSuccess(Step step) {
1002

1003
    if (step == null) {
2!
1004
      return this.context.success();
4✔
1005
    } else {
1006
      return step.asSuccess();
×
1007
    }
1008
  }
1009

1010

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

1017
    if (step == null) {
×
1018
      return this.context.error();
×
1019
    } else {
1020
      return step.asError();
×
1021
    }
1022
  }
1023
}
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