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

devonfw / IDEasy / 27616222521

16 Jun 2026 12:05PM UTC coverage: 71.325% (+0.04%) from 71.285%
27616222521

Pull #1999

github

web-flow
Merge 0d70a5896 into ec9c73a45
Pull Request #1999: #1392: smart completions

4686 of 7266 branches covered (64.49%)

Branch coverage included in aggregate %.

12104 of 16274 relevant lines covered (74.38%)

3.15 hits per line

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

73.67
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.completion.AutoCompletionRegistry;
21
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
22
import com.devonfw.tools.ide.context.IdeContext;
23
import com.devonfw.tools.ide.environment.EnvironmentVariables;
24
import com.devonfw.tools.ide.environment.EnvironmentVariablesFiles;
25
import com.devonfw.tools.ide.log.IdeLogLevel;
26
import com.devonfw.tools.ide.nls.NlsBundle;
27
import com.devonfw.tools.ide.os.MacOsHelper;
28
import com.devonfw.tools.ide.process.EnvironmentContext;
29
import com.devonfw.tools.ide.process.ProcessContext;
30
import com.devonfw.tools.ide.process.ProcessErrorHandling;
31
import com.devonfw.tools.ide.process.ProcessMode;
32
import com.devonfw.tools.ide.process.ProcessResult;
33
import com.devonfw.tools.ide.property.Property;
34
import com.devonfw.tools.ide.property.ToolArgumentsProperty;
35
import com.devonfw.tools.ide.security.ToolVersionChoice;
36
import com.devonfw.tools.ide.security.ToolVulnerabilities;
37
import com.devonfw.tools.ide.step.Step;
38
import com.devonfw.tools.ide.tool.repository.ToolRepository;
39
import com.devonfw.tools.ide.url.model.file.json.Cve;
40
import com.devonfw.tools.ide.url.model.file.json.ToolDependency;
41
import com.devonfw.tools.ide.url.model.file.json.ToolSecurity;
42
import com.devonfw.tools.ide.variable.IdeVariables;
43
import com.devonfw.tools.ide.version.GenericVersionRange;
44
import com.devonfw.tools.ide.version.VersionIdentifier;
45

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

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

53
  /** @see #getName() */
54
  protected final String tool;
55

56
  private final Set<Tag> tags;
57

58
  /** The commandline arguments to pass to the tool. */
59
  public final ToolArgumentsProperty arguments;
60

61
  private Path executionDirectory;
62

63
  private MacOsHelper macOsHelper;
64

65
  /**
66
   * Registry for tool-specific auto-completion candidates.
67
   */
68
  private AutoCompletionRegistry autoCompletionRegistry;
69

70
  /**
71
   * The constructor.
72
   *
73
   * @param context the {@link IdeContext}.
74
   * @param tool the {@link #getName() tool name}.
75
   * @param tags the {@link #getTags() tags} classifying the tool. Should be created via {@link Set#of(Object) Set.of} method.
76
   */
77
  public ToolCommandlet(IdeContext context, String tool, Set<Tag> tags) {
78

79
    super(context);
3✔
80
    this.tool = tool;
3✔
81
    this.tags = tags;
3✔
82
    addKeyword(tool);
3✔
83
    this.arguments = new ToolArgumentsProperty("", false, true, "args");
9✔
84
    initProperties();
2✔
85
  }
1✔
86

87
  /**
88
   * Gets the auto-completion registry for this tool.
89
   *
90
   * @return the {@link AutoCompletionRegistry}.
91
   */
92
  protected AutoCompletionRegistry getAutoCompletionRegistry() {
93

94
    if (this.autoCompletionRegistry == null) {
3!
95
      this.autoCompletionRegistry = new AutoCompletionRegistry();
5✔
96
      initAutoCompletionRegistry(this.autoCompletionRegistry);
4✔
97
    }
98

99
    return this.autoCompletionRegistry;
3✔
100
  }
101

102

103
  /**
104
   * Initializes the auto-completion registry for this tool.
105
   *
106
   * @param registry the {@link AutoCompletionRegistry} to initialize.
107
   */
108
  protected void initAutoCompletionRegistry(AutoCompletionRegistry registry) {
109
    // default empty
110
  }
×
111

112
  /**
113
   * Completes tool-specific arguments.
114
   *
115
   * @param arg the current argument to complete.
116
   * @param collector the {@link CompletionCandidateCollector}.
117
   * @param property the {@link Property} that triggered completion.
118
   */
119
  public void completeToolArguments(String arg, CompletionCandidateCollector collector, Property<?> property) {
120

121
    getAutoCompletionRegistry().complete(arg, collector, property, this);
7✔
122
  }
1✔
123

124
  /**
125
   * Add initial Properties to the tool
126
   */
127
  protected void initProperties() {
128

129
    add(this.arguments);
5✔
130
  }
1✔
131

132
  /**
133
   * @return the name of the tool (e.g. "java", "mvn", "npm", "node").
134
   */
135
  @Override
136
  public final String getName() {
137

138
    return this.tool;
3✔
139
  }
140

141
  /**
142
   * @return the name of the binary executable for this tool.
143
   */
144
  protected String getBinaryName() {
145

146
    return this.tool;
3✔
147
  }
148

149
  /**
150
   * @return the {@link Path} to the installed {@link #getBinaryName() binary} or {@code null} if not found on the
151
   *     {@link com.devonfw.tools.ide.common.SystemPath}.
152
   */
153
  protected Path getBinaryExecutable() {
154

155
    Path binary = this.context.getPath().findBinaryPathByName(getBinaryName());
×
156
    if (binary.getParent() == null) {
×
157
      return null;
×
158
    }
159
    return binary;
×
160
  }
161

162
  @Override
163
  public final Set<Tag> getTags() {
164

165
    return this.tags;
3✔
166
  }
167

168
  /**
169
   * @return the execution directory where the tool will be executed. Will be {@code null} by default leading to execution in the users current working
170
   *     directory where IDEasy was called.
171
   * @see #setExecutionDirectory(Path)
172
   */
173
  public Path getExecutionDirectory() {
174
    return this.executionDirectory;
×
175
  }
176

177
  /**
178
   * @param executionDirectory the new value of {@link #getExecutionDirectory()}.
179
   */
180
  public void setExecutionDirectory(Path executionDirectory) {
181
    this.executionDirectory = executionDirectory;
×
182
  }
×
183

184
  /**
185
   * @return the {@link EnvironmentVariables#getToolVersion(String) tool version}.
186
   */
187
  public VersionIdentifier getConfiguredVersion() {
188

189
    return this.context.getVariables().getToolVersion(getName());
7✔
190
  }
191

192
  /**
193
   * @return the {@link EnvironmentVariables#getToolEdition(String) tool edition}.
194
   */
195
  public String getConfiguredEdition() {
196

197
    return this.context.getVariables().getToolEdition(getName());
7✔
198
  }
199

200
  /**
201
   * @return the {@link ToolEdition} with {@link #getName() tool} with its {@link #getConfiguredEdition() edition}.
202
   */
203
  protected final ToolEdition getToolWithConfiguredEdition() {
204

205
    return new ToolEdition(this.tool, getConfiguredEdition());
8✔
206
  }
207

208
  @Override
209
  protected void doRun() {
210

211
    runTool(this.arguments.asList());
6✔
212
  }
1✔
213

214
  /**
215
   * @param args the command-line arguments to run the tool.
216
   * @return the {@link ProcessResult result}.
217
   * @see ToolCommandlet#runTool(ProcessMode, GenericVersionRange, List)
218
   */
219
  public ProcessResult runTool(List<String> args) {
220

221
    return runTool(ProcessMode.DEFAULT, null, args);
6✔
222
  }
223

224
  /**
225
   * Ensures the tool is installed and then runs this tool with the given arguments.
226
   *
227
   * @param processMode the {@link ProcessMode}. Should typically be {@link ProcessMode#DEFAULT} or {@link ProcessMode#BACKGROUND}.
228
   * @param toolVersion the explicit {@link GenericVersionRange version} to run. Typically {@code null} to run the
229
   *     {@link #getConfiguredVersion() configured version}. Otherwise, the specified version will be used (from the software repository, if not compatible).
230
   * @param args the command-line arguments to run the tool.
231
   * @return the {@link ProcessResult result}.
232
   */
233
  public final ProcessResult runTool(ProcessMode processMode, GenericVersionRange toolVersion, List<String> args) {
234

235
    return runTool(processMode, toolVersion, ProcessErrorHandling.THROW_CLI, args);
7✔
236
  }
237

238
  /**
239
   * Ensures the tool is installed and then runs this tool with the given arguments.
240
   *
241
   * @param processMode the {@link ProcessMode}. Should typically be {@link ProcessMode#DEFAULT} or {@link ProcessMode#BACKGROUND}.
242
   * @param toolVersion the explicit {@link GenericVersionRange version} to run. Typically {@code null} to run the
243
   *     {@link #getConfiguredVersion() configured version}. Otherwise, the specified version will be used (from the software repository, if not compatible).
244
   * @param errorHandling the {@link ProcessErrorHandling}.
245
   * @param args the command-line arguments to run the tool.
246
   * @return the {@link ProcessResult result}.
247
   */
248
  public ProcessResult runTool(ProcessMode processMode, GenericVersionRange toolVersion, ProcessErrorHandling errorHandling, List<String> args) {
249

250
    ProcessContext pc = this.context.newProcess().errorHandling(errorHandling);
6✔
251
    ToolInstallRequest request = new ToolInstallRequest(true);
5✔
252
    if (toolVersion != null) {
2!
253
      request.setRequested(new ToolEditionAndVersion(toolVersion));
×
254
    }
255
    request.setProcessContext(pc);
3✔
256
    return runTool(request, processMode, args);
6✔
257
  }
258

259
  /**
260
   * Ensures the tool is installed and then runs this tool with the given arguments.
261
   *
262
   * @param request the {@link ToolInstallRequest}.
263
   * @param processMode the {@link ProcessMode}. Should typically be {@link ProcessMode#DEFAULT} or {@link ProcessMode#BACKGROUND}.
264
   * @param args the command-line arguments to run the tool.
265
   * @return the {@link ProcessResult result}.
266
   */
267
  public ProcessResult runTool(ToolInstallRequest request, ProcessMode processMode, List<String> args) {
268

269
    if (request.isCveCheckDone()) {
3!
270
      // if the CVE check has already been done, we can assume that the install(request) has already been called before
271
      // most likely a postInstall* method was overridden calling this method with the same request what is a programming error
272
      // we render this warning so the error gets detected and can be fixed but we do not block the user by skipping the installation.
273
      LOG.warn("Preventing infinity loop during installation of {}", request.getRequested(), new RuntimeException());
×
274
    } else {
275
      install(request);
4✔
276
    }
277
    return runTool(request.getProcessContext(), processMode, args);
7✔
278
  }
279

280
  /**
281
   * @param pc the {@link ProcessContext}.
282
   * @param processMode the {@link ProcessMode}. Should typically be {@link ProcessMode#DEFAULT} or {@link ProcessMode#BACKGROUND}.
283
   * @param args the command-line arguments to run the tool.
284
   * @return the {@link ProcessResult result}.
285
   */
286
  public ProcessResult runTool(ProcessContext pc, ProcessMode processMode, List<String> args) {
287

288
    if (this.executionDirectory != null) {
3!
289
      pc.directory(this.executionDirectory);
×
290
    }
291
    configureToolBinary(pc, processMode);
4✔
292
    configureToolArgs(pc, processMode, args);
5✔
293
    return pc.run(processMode);
4✔
294
  }
295

296
  /**
297
   * @param pc the {@link ProcessContext}.
298
   * @param processMode the {@link ProcessMode}.
299
   */
300
  protected void configureToolBinary(ProcessContext pc, ProcessMode processMode) {
301

302
    pc.executable(Path.of(getBinaryName()));
8✔
303
  }
1✔
304

305
  /**
306
   * @param pc the {@link ProcessContext}.
307
   * @param processMode the {@link ProcessMode}.
308
   * @param args the command-line arguments to {@link ProcessContext#addArgs(List) add}.
309
   */
310
  protected void configureToolArgs(ProcessContext pc, ProcessMode processMode, List<String> args) {
311

312
    pc.addArgs(args);
4✔
313
  }
1✔
314

315
  /**
316
   * Installs or updates the managed {@link #getName() tool}.
317
   *
318
   * @return the {@link ToolInstallation}.
319
   */
320
  public ToolInstallation install() {
321

322
    return install(true);
4✔
323
  }
324

325
  /**
326
   * Performs the installation of the {@link #getName() tool} managed by this {@link com.devonfw.tools.ide.commandlet.Commandlet}.
327
   *
328
   * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
329
   * @return the {@link ToolInstallation}.
330
   */
331
  public ToolInstallation install(boolean silent) {
332
    return install(new ToolInstallRequest(silent));
7✔
333
  }
334

335
  /**
336
   * Performs the installation (install, update, downgrade) of the {@link #getName() tool} managed by this {@link ToolCommandlet}.
337
   *
338
   * @param request the {@link ToolInstallRequest}.
339
   * @return the {@link ToolInstallation}.
340
   */
341
  public ToolInstallation install(ToolInstallRequest request) {
342

343
    completeRequest(request);
3✔
344
    if (request.isInstallLoop()) {
3!
345
      return toolAlreadyInstalled(request);
×
346
    }
347
    return doInstall(request);
4✔
348
  }
349

350
  /**
351
   * Performs the installation (install, update, downgrade) of the {@link #getName() tool} managed by this {@link ToolCommandlet}.
352
   *
353
   * @param request the {@link ToolInstallRequest}.
354
   * @return the {@link ToolInstallation}.
355
   */
356
  protected abstract ToolInstallation doInstall(ToolInstallRequest request);
357

358
  /**
359
   * @param request the {@link ToolInstallRequest} to complete (fill values that are currently {@code null}).
360
   */
361
  protected void completeRequest(ToolInstallRequest request) {
362

363
    completeRequestInstalled(request);
3✔
364
    completeRequestRequested(request); // depends on completeRequestInstalled
3✔
365
    completeRequestProcessContext(request);
3✔
366
    completeRequestToolPath(request);
3✔
367
  }
1✔
368

369
  private void completeRequestProcessContext(ToolInstallRequest request) {
370
    if (request.getProcessContext() == null) {
3✔
371
      ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_CLI);
6✔
372
      request.setProcessContext(pc);
3✔
373
    }
374
  }
1✔
375

376
  private void completeRequestInstalled(ToolInstallRequest request) {
377

378
    ToolEditionAndVersion installedToolVersion = request.getInstalled();
3✔
379
    if (installedToolVersion == null) {
2✔
380
      installedToolVersion = new ToolEditionAndVersion((GenericVersionRange) null);
6✔
381
      request.setInstalled(installedToolVersion);
3✔
382
    }
383
    Path toolPath = request.getToolPath();
3✔
384
    if (installedToolVersion.getVersion() == null) {
3✔
385
      VersionIdentifier installedVersion;
386
      if ((toolPath != null) && (this instanceof LocalToolCommandlet ltc)) {
10!
387
        installedVersion = ltc.getInstalledVersion(toolPath);
5✔
388
      } else {
389
        installedVersion = getInstalledVersion();
3✔
390
      }
391
      if (installedVersion == null) {
2✔
392
        return;
1✔
393
      }
394
      installedToolVersion.setVersion(installedVersion);
3✔
395
    }
396
    if (installedToolVersion.getEdition() == null) {
3✔
397
      String installedEdition;
398
      if ((toolPath != null) && (this instanceof LocalToolCommandlet ltc)) {
2!
399
        installedEdition = ltc.getInstalledEdition(toolPath);
×
400
      } else {
401
        installedEdition = getInstalledEdition();
3✔
402
      }
403
      installedToolVersion.setEdition(new ToolEdition(this.tool, installedEdition));
8✔
404
    }
405
    assert installedToolVersion.getResolvedVersion() != null;
4!
406
  }
1✔
407

408
  private void completeRequestRequested(ToolInstallRequest request) {
409

410
    ToolEdition edition;
411
    ToolEditionAndVersion requested = request.getRequested();
3✔
412
    if (requested == null) {
2✔
413
      edition = new ToolEdition(this.tool, getConfiguredEdition());
8✔
414
      requested = new ToolEditionAndVersion(edition);
5✔
415
      request.setRequested(requested);
4✔
416

417
    } else {
418
      edition = requested.getEdition();
3✔
419
      if (edition == null) {
2✔
420
        // If no edition was specified, set it to the configured one
421
        edition = new ToolEdition(this.tool, getConfiguredEdition());
8✔
422
        requested.setEdition(edition);
3✔
423
      }
424
    }
425

426
    // Adjust edition if necessary based on requested version. This is needed for tools like IntelliJ where we may need to automatically switch editions
427
    requested = adjustRequestedEdition(requested);
4✔
428
    edition = requested.getEdition();
3✔
429

430
    GenericVersionRange version = requested.getVersion();
3✔
431
    if (version == null) {
2✔
432
      version = getConfiguredVersion();
3✔
433
      requested.setVersion(version);
3✔
434
    }
435
    VersionIdentifier resolvedVersion = requested.getResolvedVersion();
3✔
436
    if (resolvedVersion == null) {
2✔
437
      if (this.context.isSkipUpdatesMode()) {
4✔
438
        ToolEditionAndVersion installed = request.getInstalled();
3✔
439
        if (installed != null) {
2!
440
          VersionIdentifier installedVersion = installed.getResolvedVersion();
3✔
441
          if (version.contains(installedVersion)) {
4✔
442
            resolvedVersion = installedVersion;
2✔
443
          }
444
        }
445
      }
446
      if (resolvedVersion == null) {
2✔
447
        resolvedVersion = getToolRepository().resolveVersion(this.tool, edition.edition(), version, this);
10✔
448
      }
449
      requested.setResolvedVersion(resolvedVersion);
3✔
450
    }
451
  }
1✔
452

453
  /**
454
   * Hook for subclasses to adjust the requested tool edition before the version is finalized.
455
   *
456
   * @param requested the requested {@link ToolEditionAndVersion}
457
   * @return the given or trgansformed {@link ToolEditionAndVersion}
458
   */
459
  protected ToolEditionAndVersion adjustRequestedEdition(ToolEditionAndVersion requested) {
460

461
    // default no-op
462
    return requested;
2✔
463
  }
464

465

466
  private void completeRequestToolPath(ToolInstallRequest request) {
467

468
    Path toolPath = request.getToolPath();
3✔
469
    if (toolPath == null) {
2✔
470
      toolPath = getToolPath();
3✔
471
      request.setToolPath(toolPath);
3✔
472
    }
473
  }
1✔
474

475
  /**
476
   * @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
477
   *     determined by the installer.
478
   */
479
  public Path getToolPath() {
480
    return null;
×
481
  }
482

483
  /**
484
   * This method is called after a tool was requested to be installed or updated.
485
   *
486
   * @param request {@code true} the {@link ToolInstallRequest}.
487
   */
488
  protected void postInstall(ToolInstallRequest request) {
489

490
    if (!request.isAlreadyInstalled()) {
3✔
491
      postInstallOnNewInstallation(request);
3✔
492
    }
493
  }
1✔
494

495
  /**
496
   * This method is called after a tool was requested to be installed or updated and a new installation was performed.
497
   *
498
   * @param request {@code true} the {@link ToolInstallRequest}.
499
   */
500
  protected void postInstallOnNewInstallation(ToolInstallRequest request) {
501

502
    // nothing to do by default
503
  }
1✔
504

505
  /**
506
   * @param edition the {@link #getInstalledEdition() edition}.
507
   * @param version the {@link #getInstalledVersion() version}.
508
   * @return the {@link Path} where this tool is installed (physically) or {@code null} if not available.
509
   */
510
  protected abstract Path getInstallationPath(String edition, VersionIdentifier version);
511

512
  /**
513
   * @param request the {@link ToolInstallRequest}.
514
   * @return the existing {@link ToolInstallation}.
515
   */
516
  protected ToolInstallation createExistingToolInstallation(ToolInstallRequest request) {
517

518
    ToolEditionAndVersion installed = request.getInstalled();
3✔
519

520
    String edition = this.tool;
3✔
521
    VersionIdentifier resolvedVersion = VersionIdentifier.LATEST;
2✔
522

523
    if (installed != null) {
2!
524
      if (installed.getEdition() != null) {
3!
525
        edition = installed.getEdition().edition();
4✔
526
      }
527
      if (installed.getResolvedVersion() != null) {
3!
528
        resolvedVersion = installed.getResolvedVersion();
3✔
529
      }
530
    }
531

532
    return createExistingToolInstallation(edition, resolvedVersion, request.getProcessContext(),
8✔
533
        request.isAdditionalInstallation());
1✔
534
  }
535

536
  /**
537
   * @param edition the {@link #getConfiguredEdition() edition}.
538
   * @param installedVersion the {@link #getConfiguredVersion() version}.
539
   * @param environmentContext the {@link EnvironmentContext}.
540
   * @param extraInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
541
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
542
   * @return the {@link ToolInstallation}.
543
   */
544
  protected ToolInstallation createExistingToolInstallation(String edition, VersionIdentifier installedVersion, EnvironmentContext environmentContext,
545
      boolean extraInstallation) {
546

547
    Path installationPath = getInstallationPath(edition, installedVersion);
5✔
548
    return createToolInstallation(installationPath, installedVersion, false, environmentContext, extraInstallation);
8✔
549
  }
550

551
  /**
552
   * @param rootDir the {@link ToolInstallation#rootDir() top-level installation directory}.
553
   * @param version the installed {@link VersionIdentifier}.
554
   * @param newInstallation {@link ToolInstallation#newInstallation() new installation} flag.
555
   * @param environmentContext the {@link EnvironmentContext}.
556
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
557
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
558
   * @return the {@link ToolInstallation}.
559
   */
560
  protected ToolInstallation createToolInstallation(Path rootDir, VersionIdentifier version, boolean newInstallation,
561
      EnvironmentContext environmentContext, boolean additionalInstallation) {
562

563
    Path linkDir = rootDir;
2✔
564
    Path binDir = rootDir;
2✔
565
    if (rootDir != null) {
2!
566
      // on MacOS applications have a very strange structure - see JavaDoc of findLinkDir and ToolInstallation.linkDir for details.
567
      linkDir = getMacOsHelper().findLinkDir(rootDir, getBinaryName());
7✔
568
      binDir = this.context.getFileAccess().getBinPath(linkDir);
6✔
569
    }
570
    return createToolInstallation(rootDir, linkDir, binDir, version, newInstallation, environmentContext, additionalInstallation);
10✔
571
  }
572

573
  /**
574
   * @param rootDir the {@link ToolInstallation#rootDir() top-level installation directory}.
575
   * @param linkDir the {@link ToolInstallation#linkDir() link directory}.
576
   * @param binDir the {@link ToolInstallation#binDir() bin directory}.
577
   * @param version the installed {@link VersionIdentifier}.
578
   * @param newInstallation {@link ToolInstallation#newInstallation() new installation} flag.
579
   * @param environmentContext the {@link EnvironmentContext}.
580
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
581
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
582
   * @return the {@link ToolInstallation}.
583
   */
584
  protected ToolInstallation createToolInstallation(Path rootDir, Path linkDir, Path binDir, VersionIdentifier version, boolean newInstallation,
585
      EnvironmentContext environmentContext, boolean additionalInstallation) {
586

587
    // do not copy the version file into macOS .app bundles: changing the bundle after codesigning breaks the seal.
588
    ToolInstallation toolInstallation = new ToolInstallation(rootDir, linkDir, binDir, version, newInstallation);
9✔
589
    setEnvironment(environmentContext, toolInstallation, additionalInstallation);
5✔
590
    return toolInstallation;
2✔
591
  }
592

593
  /**
594
   * Called if the tool {@link ToolInstallRequest#isAlreadyInstalled() is already installed in the correct edition and version} so we can skip the
595
   * installation.
596
   *
597
   * @param request the {@link ToolInstallRequest}.
598
   * @return the {@link ToolInstallation}.
599
   */
600
  protected ToolInstallation toolAlreadyInstalled(ToolInstallRequest request) {
601

602
    logToolAlreadyInstalled(request);
3✔
603
    cveCheck(request);
4✔
604
    postInstall(request);
3✔
605
    return createExistingToolInstallation(request);
4✔
606
  }
607

608
  /**
609
   * Log that the tool is already installed.
610
   *
611
   * @param request the {@link ToolInstallRequest}.
612
   */
613
  protected void logToolAlreadyInstalled(ToolInstallRequest request) {
614
    Level level;
615
    if (request.isSilent()) {
3✔
616
      level = Level.DEBUG;
3✔
617
    } else {
618
      level = Level.INFO;
2✔
619
    }
620
    ToolEditionAndVersion installed = request.getInstalled();
3✔
621
    LOG.atLevel(level).log("Version {} of tool {} is already installed", installed.getVersion(), installed.getEdition());
9✔
622
  }
1✔
623

624
  /**
625
   * Method to get the home path of the given {@link ToolInstallation}.
626
   *
627
   * @param toolInstallation the {@link ToolInstallation}.
628
   * @return the Path to the home of the tool
629
   */
630
  protected Path getToolHomePath(ToolInstallation toolInstallation) {
631
    return toolInstallation.linkDir();
3✔
632
  }
633

634
  /**
635
   * Method to set environment variables for the process context.
636
   *
637
   * @param environmentContext the {@link EnvironmentContext} where to {@link EnvironmentContext#withEnvVar(String, String) set environment variables} for
638
   *     this tool.
639
   * @param toolInstallation the {@link ToolInstallation}.
640
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
641
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
642
   */
643
  public void setEnvironment(EnvironmentContext environmentContext, ToolInstallation toolInstallation, boolean additionalInstallation) {
644

645
    String pathVariable = EnvironmentVariables.getToolVariablePrefix(this.tool) + "_HOME";
5✔
646
    Path toolHomePath = getToolHomePath(toolInstallation);
4✔
647
    if (toolHomePath != null) {
2!
648
      environmentContext.withEnvVar(pathVariable, toolHomePath.toString());
6✔
649
    }
650
    if (additionalInstallation) {
2✔
651
      environmentContext.withPathEntry(toolInstallation.binDir());
5✔
652
    }
653
  }
1✔
654

655
  /**
656
   * @return {@code true} to extract (unpack) the downloaded binary file, {@code false} otherwise.
657
   */
658
  protected boolean isExtract() {
659

660
    return true;
2✔
661
  }
662

663
  /**
664
   * 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
665
   * something better, we will suggest this to the user and ask him to make his choice.
666
   *
667
   * @param request the {@link ToolInstallRequest}.
668
   * @return the {@link VersionIdentifier} to install. The will may be asked (unless {@code skipSuggestions} is {@code true}) and might choose a different
669
   *     version than the originally requested one.
670
   */
671
  protected VersionIdentifier cveCheck(ToolInstallRequest request) {
672

673
    ToolEditionAndVersion requested = request.getRequested();
3✔
674
    VersionIdentifier resolvedVersion = requested.getResolvedVersion();
3✔
675
    if (request.isCveCheckDone()) {
3✔
676
      return resolvedVersion;
2✔
677
    }
678
    ToolEdition toolEdition = requested.getEdition();
3✔
679
    GenericVersionRange allowedVersions = requested.getVersion();
3✔
680
    boolean requireStableVersion = true;
2✔
681
    if (allowedVersions instanceof VersionIdentifier vi) {
6✔
682
      requireStableVersion = vi.isStable();
3✔
683
    }
684
    ToolSecurity toolSecurity = this.context.getDefaultToolRepository().findSecurity(this.tool, toolEdition.edition());
9✔
685
    double minSeverity = IdeVariables.CVE_MIN_SEVERITY.get(context);
7✔
686
    ToolVulnerabilities currentVulnerabilities = toolSecurity.findCves(resolvedVersion, minSeverity);
5✔
687
    ToolVersionChoice currentChoice = ToolVersionChoice.ofCurrent(requested, currentVulnerabilities);
4✔
688
    request.setCveCheckDone();
2✔
689
    if (currentChoice.logAndCheckIfEmpty()) {
3✔
690
      return resolvedVersion;
2✔
691
    }
692
    boolean alreadyInstalled = request.isAlreadyInstalled();
3✔
693
    boolean directForceInstall = this.context.isForceMode() && request.isDirect();
6!
694
    if (alreadyInstalled && !directForceInstall) {
2!
695
      // currently for a transitive dependency it does not make sense to suggest alternative versions, since the choice is not stored anywhere,
696
      // 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
697
      // (e.g. upgrade the tool with the dependency that is causing this).
698
      IdeLogLevel.INTERACTION.log(LOG, "Please run 'ide -f install {}' to check for update suggestions!", this.tool);
×
699
      return resolvedVersion;
×
700
    }
701
    ToolVersionChoice latest = null;
2✔
702
    ToolVulnerabilities latestVulnerabilities = currentVulnerabilities;
2✔
703
    ToolVersionChoice nearest = null;
2✔
704
    ToolVulnerabilities nearestVulnerabilities = currentVulnerabilities;
2✔
705
    List<VersionIdentifier> toolVersions = getVersions();
3✔
706
    for (VersionIdentifier version : toolVersions) {
10✔
707

708
      if (Objects.equals(version, resolvedVersion)) {
4✔
709
        continue; // Skip the entire iteration for resolvedVersion
1✔
710
      }
711

712
      if (acceptVersion(version, allowedVersions, requireStableVersion)) {
5!
713
        ToolVulnerabilities newVulnerabilities = toolSecurity.findCves(version, minSeverity);
5✔
714
        if (newVulnerabilities.isSafer(latestVulnerabilities)) {
4✔
715
          // we found a better/safer version
716
          ToolEditionAndVersion toolEditionAndVersion = new ToolEditionAndVersion(toolEdition, version);
6✔
717
          if (version.isGreater(resolvedVersion)) {
4!
718
            latestVulnerabilities = newVulnerabilities;
2✔
719
            latest = ToolVersionChoice.ofLatest(toolEditionAndVersion, latestVulnerabilities);
4✔
720
            nearest = null;
3✔
721
          } else {
722
            nearestVulnerabilities = newVulnerabilities;
×
723
            nearest = ToolVersionChoice.ofNearest(toolEditionAndVersion, nearestVulnerabilities);
×
724
          }
725
        } else if (newVulnerabilities.isSaferOrEqual(nearestVulnerabilities)) {
5✔
726
          if (newVulnerabilities.isSafer(nearestVulnerabilities) || version.isGreater(resolvedVersion)) {
8!
727
            nearest = ToolVersionChoice.ofNearest(new ToolEditionAndVersion(toolEdition, version), newVulnerabilities);
8✔
728
          }
729
          nearestVulnerabilities = newVulnerabilities;
2✔
730
        }
731
      }
732
    }
1✔
733
    if ((latest == null) && (nearest == null)) {
2!
734
      LOG.warn(
×
735
          "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.");
736
      if (alreadyInstalled) {
×
737
        // we came here via "ide -f install ..." but no alternative is available
738
        return resolvedVersion;
×
739
      }
740
    }
741
    List<ToolVersionChoice> choices = new ArrayList<>();
4✔
742
    choices.add(currentChoice);
4✔
743
    boolean addSuggestions;
744
    if (this.context.isForceMode() && request.isDirect()) {
4!
745
      addSuggestions = true;
×
746
    } else {
747
      List<String> skipCveFixTools = IdeVariables.SKIP_CVE_FIX.get(this.context);
6✔
748
      addSuggestions = !skipCveFixTools.contains(this.tool);
8!
749
    }
750
    if (nearest != null) {
2!
751
      if (addSuggestions) {
2!
752
        choices.add(nearest);
4✔
753
      }
754
      nearest.logAndCheckIfEmpty();
3✔
755
    }
756
    if (latest != null) {
2!
757
      if (addSuggestions) {
2!
758
        choices.add(latest);
4✔
759
      }
760
      latest.logAndCheckIfEmpty();
3✔
761
    }
762
    ToolVersionChoice[] choicesArray = choices.toArray(ToolVersionChoice[]::new);
8✔
763
    LOG.warn("Please note that by selecting an unsafe version to install, you accept the risk to be attacked.");
3✔
764
    ToolVersionChoice answer = this.context.question(choicesArray, "Which version do you want to install?");
9✔
765
    VersionIdentifier version = answer.toolEditionAndVersion().getResolvedVersion();
4✔
766
    requested.setResolvedVersion(version);
3✔
767
    return version;
2✔
768
  }
769

770
  private static boolean acceptVersion(VersionIdentifier version, GenericVersionRange allowedVersions, boolean requireStableVersion) {
771
    if (allowedVersions.isPattern() && !allowedVersions.contains(version)) {
3!
772
      return false;
×
773
    } else if (requireStableVersion && !version.isStable()) {
5!
774
      return false;
×
775
    }
776
    return true;
2✔
777
  }
778

779
  /**
780
   * @return the {@link MacOsHelper} instance.
781
   */
782
  protected MacOsHelper getMacOsHelper() {
783

784
    if (this.macOsHelper == null) {
3✔
785
      this.macOsHelper = new MacOsHelper(this.context);
7✔
786
    }
787
    return this.macOsHelper;
3✔
788
  }
789

790
  /**
791
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
792
   */
793
  public abstract VersionIdentifier getInstalledVersion();
794

795
  /**
796
   * @return {@code true} if this tool is installed, {@code false} otherwise.
797
   */
798
  public boolean isInstalled() {
799

800
    return getInstalledVersion() != null;
7✔
801
  }
802

803
  /**
804
   * @return the installed edition of this tool or {@code null} if not installed.
805
   */
806
  public abstract String getInstalledEdition();
807

808
  /**
809
   * Uninstalls the {@link #getName() tool}.
810
   */
811
  public abstract void uninstall();
812

813
  /**
814
   * @return the {@link ToolRepository}.
815
   */
816
  public ToolRepository getToolRepository() {
817

818
    return this.context.getDefaultToolRepository();
4✔
819
  }
820

821
  /**
822
   * List the available editions of this tool.
823
   */
824
  public void listEditions() {
825

826
    List<String> editions = getToolRepository().getSortedEditions(getName());
6✔
827
    for (String edition : editions) {
10✔
828
      LOG.info(edition);
3✔
829
    }
1✔
830
  }
1✔
831

832
  /**
833
   * List the available versions of this tool.
834
   */
835
  public void listVersions() {
836

837
    List<VersionIdentifier> versions = getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
838
    for (VersionIdentifier vi : versions) {
10✔
839
      LOG.info(vi.toString());
4✔
840
    }
1✔
841
  }
1✔
842

843
  /**
844
   * @return the {@link com.devonfw.tools.ide.tool.repository.DefaultToolRepository#getSortedVersions(String, String, ToolCommandlet) sorted versions} of this
845
   *     tool.
846
   */
847
  public List<VersionIdentifier> getVersions() {
848
    return getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
849
  }
850

851
  /**
852
   * Sets the tool version in the environment variable configuration file.
853
   *
854
   * @param version the version (pattern) to set.
855
   */
856
  public void setVersion(String version) {
857

858
    if ((version == null) || version.isBlank()) {
×
859
      throw new IllegalStateException("Version has to be specified!");
×
860
    }
861
    VersionIdentifier configuredVersion = VersionIdentifier.of(version);
×
862
    if (!configuredVersion.isPattern() && !configuredVersion.isValid()) {
×
863
      LOG.warn("Version {} seems to be invalid", version);
×
864
    }
865
    setVersion(configuredVersion, true);
×
866
  }
×
867

868
  /**
869
   * Sets the tool version in the environment variable configuration file.
870
   *
871
   * @param version the version to set. May also be a {@link VersionIdentifier#isPattern() version pattern}.
872
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
873
   */
874
  public void setVersion(VersionIdentifier version, boolean hint) {
875

876
    setVersion(version, hint, null);
5✔
877
  }
1✔
878

879
  /**
880
   * Sets the tool version in the environment variable configuration file.
881
   *
882
   * @param version the version to set. May also be a {@link VersionIdentifier#isPattern() version pattern}.
883
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
884
   * @param destination - the destination for the property to be set
885
   */
886
  public void setVersion(VersionIdentifier version, boolean hint, EnvironmentVariablesFiles destination) {
887

888
    String edition = getConfiguredEdition();
3✔
889
    ToolRepository toolRepository = getToolRepository();
3✔
890

891
    EnvironmentVariables variables = this.context.getVariables();
4✔
892
    if (destination == null) {
2✔
893
      //use default location
894
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
895
    }
896
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
897
    String name = EnvironmentVariables.getToolVersionVariable(this.tool);
4✔
898

899
    toolRepository.resolveVersion(this.tool, edition, version, this); // verify that the version actually exists
8✔
900
    settingsVariables.set(name, version.toString(), false);
7✔
901
    settingsVariables.save();
2✔
902
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
903
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
904
      LOG.warn("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name,
5✔
905
          declaringVariables.getSource());
1✔
906
    }
907
    if (hint) {
2✔
908
      LOG.info("To install that version call the following command:");
3✔
909
      LOG.info("ide install {}", this.tool);
5✔
910
    }
911
  }
1✔
912

913
  /**
914
   * Sets the tool edition in the environment variable configuration file.
915
   *
916
   * @param edition the edition to set.
917
   */
918
  public void setEdition(String edition) {
919

920
    setEdition(edition, true);
4✔
921
  }
1✔
922

923
  /**
924
   * Sets the tool edition in the environment variable configuration file.
925
   *
926
   * @param edition the edition to set
927
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
928
   */
929
  public void setEdition(String edition, boolean hint) {
930

931
    setEdition(edition, hint, null);
5✔
932
  }
1✔
933

934
  /**
935
   * Sets the tool edition in the environment variable configuration file.
936
   *
937
   * @param edition the edition to set
938
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
939
   * @param destination - the destination for the property to be set
940
   */
941
  public void setEdition(String edition, boolean hint, EnvironmentVariablesFiles destination) {
942

943
    if ((edition == null) || edition.isBlank()) {
5!
944
      throw new IllegalStateException("Edition has to be specified!");
×
945
    }
946

947
    if (destination == null) {
2✔
948
      //use default location
949
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
950
    }
951

952
    if (!getToolRepository().getSortedEditions(this.tool).contains(edition)) {
8✔
953
      LOG.warn("Edition {} seems to be invalid", edition);
4✔
954
    }
955
    EnvironmentVariables variables = this.context.getVariables();
4✔
956
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
957
    String name = EnvironmentVariables.getToolEditionVariable(this.tool);
4✔
958
    settingsVariables.set(name, edition, false);
6✔
959
    settingsVariables.save();
2✔
960

961
    LOG.info("{}={} has been set in {}", name, edition, settingsVariables.getSource());
18✔
962
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
963
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
964
      LOG.warn("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name,
5✔
965
          declaringVariables.getSource());
1✔
966
    }
967
    if (hint) {
2!
968
      LOG.info("To install that edition call the following command:");
3✔
969
      LOG.info("ide install {}", this.tool);
5✔
970
    }
971
  }
1✔
972

973
  /**
974
   * Runs the tool's help command to provide the user with usage information.
975
   */
976
  @Override
977
  public void printHelp(NlsBundle bundle) {
978

979
    super.printHelp(bundle);
3✔
980
    String toolHelpArgs = getToolHelpArguments();
3✔
981
    if (toolHelpArgs != null && getInstalledVersion() != null) {
5!
982
      ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.LOG_WARNING)
6✔
983
          .executable(Path.of(getBinaryName())).addArgs(toolHelpArgs);
13✔
984
      pc.run(ProcessMode.DEFAULT);
4✔
985
    }
986
  }
1✔
987

988
  /**
989
   * @return the tool's specific help command. Usually help, --help or -h. Return null if not applicable.
990
   */
991
  public String getToolHelpArguments() {
992

993
    return null;
×
994
  }
995

996
  /**
997
   * Creates a start script for the tool using the tool name.
998
   *
999
   * @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
1000
   *     instead.
1001
   * @param binary name of the binary to execute from the start script.
1002
   */
1003
  protected void createStartScript(Path targetDir, String binary) {
1004

1005
    createStartScript(targetDir, binary, false);
×
1006
  }
×
1007

1008
  /**
1009
   * Creates a start script for the tool using the tool name.
1010
   *
1011
   * @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
1012
   *     instead.
1013
   * @param binary name of the binary to execute from the start script.
1014
   * @param background {@code true} to run the {@code binary} in background, {@code false} otherwise (foreground).
1015
   */
1016
  protected void createStartScript(Path targetDir, String binary, boolean background) {
1017

1018
    Path binFolder = targetDir.resolve("bin");
×
1019
    if (!Files.exists(binFolder)) {
×
1020
      if (this.context.getSystemInfo().isMac()) {
×
1021
        MacOsHelper macOsHelper = getMacOsHelper();
×
1022
        Path appDir = macOsHelper.findAppDir(targetDir);
×
1023
        binFolder = macOsHelper.findLinkDir(appDir, binary);
×
1024
      } else {
×
1025
        binFolder = targetDir;
×
1026
      }
1027
      assert (Files.exists(binFolder));
×
1028
    }
1029
    Path bashFile = binFolder.resolve(getName());
×
1030
    String bashFileContentStart = "#!/usr/bin/env bash\n\"$(dirname \"$0\")/";
×
1031
    String bashFileContentEnd = "\" $@";
×
1032
    if (background) {
×
1033
      bashFileContentEnd += " &";
×
1034
    }
1035
    try {
1036
      Files.writeString(bashFile, bashFileContentStart + binary + bashFileContentEnd);
×
1037
    } catch (IOException e) {
×
1038
      throw new RuntimeException(e);
×
1039
    }
×
1040
    assert (Files.exists(bashFile));
×
1041
    context.getFileAccess().makeExecutable(bashFile);
×
1042
  }
×
1043

1044
  @Override
1045
  public void reset() {
1046
    super.reset();
2✔
1047
    this.executionDirectory = null;
3✔
1048
  }
1✔
1049

1050
  /**
1051
   * @param command the binary that will be searched in the PATH e.g. docker
1052
   * @return true if the command is available to use
1053
   */
1054
  protected boolean isCommandAvailable(String command) {
1055
    return this.context.getPath().hasBinaryOnPath(command);
×
1056
  }
1057

1058
  /**
1059
   * @param output the raw output string from executed command e.g. 'docker version'
1060
   * @param pattern Regular Expression pattern that filters out the unnecessary texts.
1061
   * @return version that has been processed.
1062
   */
1063
  protected VersionIdentifier resolveVersionWithPattern(String output, Pattern pattern) {
1064
    Matcher matcher = pattern.matcher(output);
×
1065

1066
    if (matcher.find()) {
×
1067
      return VersionIdentifier.of(matcher.group(1));
×
1068
    } else {
1069
      return null;
×
1070
    }
1071
  }
1072

1073
  /**
1074
   * @deprecated directly log success message and then report success on step if not null.
1075
   */
1076
  @Deprecated
1077
  protected void success(Step step, String message, Object... args) {
1078

1079
    if (step == null) {
×
1080
      IdeLogLevel.SUCCESS.log(LOG, message, args);
×
1081
    } else {
1082
      step.success(message, args);
×
1083
    }
1084
  }
×
1085

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