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

devonfw / IDEasy / 20433382941

22 Dec 2025 01:31PM UTC coverage: 69.904% (-0.2%) from 70.088%
20433382941

Pull #1668

github

web-flow
Merge b8d300f26 into 8cc587d42
Pull Request #1668: #1653 implementation get version and get edition docker

3981 of 6272 branches covered (63.47%)

Branch coverage included in aggregate %.

10192 of 14003 relevant lines covered (72.78%)

3.14 hits per line

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

74.5
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
  }
1✔
304

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

312
  private void completeRequestInstalled(ToolInstallRequest request) {
313

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

332
  private void completeRequestRequested(ToolInstallRequest request) {
333

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

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

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

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

389
    // nothing to do by default
390
  }
1✔
391

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

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

405
    ToolEditionAndVersion installed = request.getInstalled();
3✔
406

407
    String edition = this.tool;
3✔
408
    VersionIdentifier resolvedVersion = VersionIdentifier.LATEST;
2✔
409

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

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

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

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

438
  /**
439
   * @param rootDir the {@link ToolInstallation#rootDir() top-level installation directory}.
440
   * @param version the installed {@link VersionIdentifier}.
441
   * @param newInstallation {@link ToolInstallation#newInstallation() new installation} flag.
442
   * @param environmentContext the {@link EnvironmentContext}.
443
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
444
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
445
   * @return the {@link ToolInstallation}.
446
   */
447
  protected ToolInstallation createToolInstallation(Path rootDir, VersionIdentifier version, boolean newInstallation,
448
      EnvironmentContext environmentContext, boolean additionalInstallation) {
449

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

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

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

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

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

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

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

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

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

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

553
    return true;
2✔
554
  }
555

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

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

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

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

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

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

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

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

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

694
    return getInstalledVersion() != null;
7✔
695
  }
696

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

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

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

712
    return this.context.getDefaultToolRepository();
4✔
713
  }
714

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

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

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

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

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

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

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

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

770
    setVersion(version, hint, null);
5✔
771
  }
1✔
772

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

782
    String edition = getConfiguredEdition();
3✔
783
    ToolRepository toolRepository = getToolRepository();
3✔
784

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

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

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

814
    setEdition(edition, true);
4✔
815
  }
1✔
816

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

825
    setEdition(edition, hint, null);
5✔
826
  }
1✔
827

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

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

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

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

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

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

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

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

887
    return null;
×
888
  }
889

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

899
    createStartScript(targetDir, binary, false);
×
900
  }
×
901

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

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

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

944
  /**
945
   * @param command the binary that will be searched in the PATH e.g. docker
946
   * @return true if the command is available to use
947
   */
948
  protected boolean isCommandAvailable(String command) {
949
    return this.context.getPath().hasBinaryOnPath(command);
×
950
  }
951

952
  /**
953
   * @param output the raw output string from executed command e.g. 'docker version'
954
   * @param pattern Regular Expression pattern that filters out the unnecessary texts.
955
   * @return version that has been processed.
956
   */
957
  protected VersionIdentifier resolveVersionWithPattern(String output, Pattern pattern) {
958
    Matcher matcher = pattern.matcher(output);
×
959

960
    if (matcher.find()) {
×
961
      return VersionIdentifier.of(matcher.group(1));
×
962
    } else {
963
      return null;
×
964
    }
965
  }
966

967
  /**
968
   * @param step the {@link Step} to get {@link Step#asSuccess() success logger} from. May be {@code null}.
969
   * @return the {@link IdeSubLogger} from {@link Step#asSuccess()} or {@link IdeContext#success()} as fallback.
970
   */
971
  protected IdeSubLogger asSuccess(Step step) {
972

973
    if (step == null) {
2!
974
      return this.context.success();
4✔
975
    } else {
976
      return step.asSuccess();
×
977
    }
978
  }
979

980

981
  /**
982
   * @param step the {@link Step} to get {@link Step#asError() error logger} from. May be {@code null}.
983
   * @return the {@link IdeSubLogger} from {@link Step#asError()} or {@link IdeContext#error()} as fallback.
984
   */
985
  protected IdeSubLogger asError(Step step) {
986

987
    if (step == null) {
×
988
      return this.context.error();
×
989
    } else {
990
      return step.asError();
×
991
    }
992
  }
993
}
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