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

devonfw / IDEasy / 30071790016

24 Jul 2026 06:16AM UTC coverage: 72.586% (+0.03%) from 72.561%
30071790016

Pull #2204

github

web-flow
Merge 8c6327fc3 into dd45e9162
Pull Request #2204: #2192-support-for-auto-completion-synonyms

4990 of 7598 branches covered (65.68%)

Branch coverage included in aggregate %.

12856 of 16988 relevant lines covered (75.68%)

3.2 hits per line

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

74.08
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.process.ProcessResultImpl;
34
import com.devonfw.tools.ide.property.Property;
35
import com.devonfw.tools.ide.property.ToolArgumentsProperty;
36
import com.devonfw.tools.ide.security.ToolVersionChoice;
37
import com.devonfw.tools.ide.security.ToolVulnerabilities;
38
import com.devonfw.tools.ide.step.Step;
39
import com.devonfw.tools.ide.tool.repository.ToolRepository;
40
import com.devonfw.tools.ide.url.model.file.json.Cve;
41
import com.devonfw.tools.ide.url.model.file.json.ToolDependency;
42
import com.devonfw.tools.ide.url.model.file.json.ToolSecurity;
43
import com.devonfw.tools.ide.variable.IdeVariables;
44
import com.devonfw.tools.ide.version.GenericVersionRange;
45
import com.devonfw.tools.ide.version.VersionIdentifier;
46

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

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

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

57
  private final Set<Tag> tags;
58

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

62
  private Path executionDirectory;
63

64
  private MacOsHelper macOsHelper;
65

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

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

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

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

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

100
    return this.autoCompletionRegistry;
3✔
101
  }
102

103

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

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

122
    getAutoCompletionRegistry().complete(arg, collector, property, this, collector.getAlreadyProvided());
9✔
123
  }
1✔
124

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

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

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

139
    return this.tool;
3✔
140
  }
141

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

147
    return this.tool;
3✔
148
  }
149

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

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

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

166
    return this.tags;
3✔
167
  }
168

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

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

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

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

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

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

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

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

209
  @Override
210
  protected void doRun() {
211

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

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

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

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

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

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

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

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

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

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

292
    if (this.executionDirectory != null) {
3!
293
      pc.directory(this.executionDirectory);
×
294
    }
295
    configureToolBinary(pc, processMode);
4✔
296
    configureToolArgs(pc, processMode, args);
5✔
297
    return pc.run(processMode);
4✔
298
  }
299

300
  /**
301
   * @param pc the {@link ProcessContext}.
302
   * @param processMode the {@link ProcessMode}.
303
   */
304
  protected void configureToolBinary(ProcessContext pc, ProcessMode processMode) {
305

306
    pc.executable(Path.of(getBinaryName()));
8✔
307
  }
1✔
308

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

316
    pc.addArgs(args);
4✔
317
  }
1✔
318

319
  /**
320
   * Installs or updates the managed {@link #getName() tool}.
321
   *
322
   * @return the {@link ToolInstallation}.
323
   */
324
  public ToolInstallation install() {
325

326
    return install(true);
4✔
327
  }
328

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

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

347
    completeRequest(request);
3✔
348
    if (request.isInstallLoop()) {
3!
349
      return toolAlreadyInstalled(request);
×
350
    }
351
    ToolInstallation installation = doInstall(request);
4✔
352
    if (installation != null && installation.installedAsynchronously()) {
5!
353
      LOG.warn(
4✔
354
          "The installation of {} is currently running in the background!\n"
355
              + "You need to complete the installation, potentially reboot and rerun your 'ide' command in a new terminal session"
356
              + " after the installation has completed.",
357
          request.getRequested());
1✔
358
    }
359
    return installation;
2✔
360
  }
361

362
  /**
363
   * Performs the installation (install, update, downgrade) of the {@link #getName() tool} managed by this {@link ToolCommandlet}.
364
   *
365
   * @param request the {@link ToolInstallRequest}.
366
   * @return the {@link ToolInstallation}.
367
   */
368
  protected abstract ToolInstallation doInstall(ToolInstallRequest request);
369

370
  /**
371
   * @param request the {@link ToolInstallRequest} to complete (fill values that are currently {@code null}).
372
   */
373
  protected void completeRequest(ToolInstallRequest request) {
374

375
    completeRequestInstalled(request);
3✔
376
    completeRequestRequested(request); // depends on completeRequestInstalled
3✔
377
    completeRequestProcessContext(request);
3✔
378
    completeRequestToolPath(request);
3✔
379
  }
1✔
380

381
  private void completeRequestProcessContext(ToolInstallRequest request) {
382
    if (request.getProcessContext() == null) {
3✔
383
      ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_CLI);
6✔
384
      request.setProcessContext(pc);
3✔
385
    }
386
  }
1✔
387

388
  private void completeRequestInstalled(ToolInstallRequest request) {
389

390
    ToolEditionAndVersion installedToolVersion = request.getInstalled();
3✔
391
    if (installedToolVersion == null) {
2✔
392
      installedToolVersion = new ToolEditionAndVersion((GenericVersionRange) null);
6✔
393
      request.setInstalled(installedToolVersion);
3✔
394
    }
395
    Path toolPath = request.getToolPath();
3✔
396
    if (installedToolVersion.getVersion() == null) {
3✔
397
      VersionIdentifier installedVersion;
398
      if ((toolPath != null) && (this instanceof LocalToolCommandlet ltc)) {
10!
399
        installedVersion = ltc.getInstalledVersion(toolPath);
5✔
400
      } else {
401
        installedVersion = getInstalledVersion();
3✔
402
      }
403
      if (installedVersion == null) {
2✔
404
        return;
1✔
405
      }
406
      installedToolVersion.setVersion(installedVersion);
3✔
407
    }
408
    if (installedToolVersion.getEdition() == null) {
3✔
409
      String installedEdition;
410
      if ((toolPath != null) && (this instanceof LocalToolCommandlet ltc)) {
2!
411
        installedEdition = ltc.getInstalledEdition(toolPath);
×
412
      } else {
413
        installedEdition = getInstalledEdition();
3✔
414
      }
415
      installedToolVersion.setEdition(new ToolEdition(this.tool, installedEdition));
8✔
416
    }
417
    assert installedToolVersion.getResolvedVersion() != null;
4!
418
  }
1✔
419

420
  private void completeRequestRequested(ToolInstallRequest request) {
421

422
    ToolEdition edition;
423
    ToolEditionAndVersion requested = request.getRequested();
3✔
424
    if (requested == null) {
2✔
425
      edition = new ToolEdition(this.tool, getConfiguredEdition());
8✔
426
      requested = new ToolEditionAndVersion(edition);
5✔
427
      request.setRequested(requested);
4✔
428

429
    } else {
430
      edition = requested.getEdition();
3✔
431
      if (edition == null) {
2✔
432
        // If no edition was specified, set it to the configured one
433
        edition = new ToolEdition(this.tool, getConfiguredEdition());
8✔
434
        requested.setEdition(edition);
3✔
435
      }
436
    }
437

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

442
    GenericVersionRange version = requested.getVersion();
3✔
443
    if (version == null) {
2✔
444
      version = getConfiguredVersion();
3✔
445
      requested.setVersion(version);
3✔
446
    }
447
    VersionIdentifier resolvedVersion = requested.getResolvedVersion();
3✔
448
    if (resolvedVersion == null) {
2✔
449
      if (this.context.isSkipUpdatesMode()) {
4✔
450
        ToolEditionAndVersion installed = request.getInstalled();
3✔
451
        if (installed != null) {
2!
452
          VersionIdentifier installedVersion = installed.getResolvedVersion();
3✔
453
          if (version.contains(installedVersion)) {
4✔
454
            resolvedVersion = installedVersion;
2✔
455
          }
456
        }
457
      }
458
      if (resolvedVersion == null) {
2✔
459
        resolvedVersion = getToolRepository().resolveVersion(this.tool, edition.edition(), version, this);
10✔
460
      }
461
      requested.setResolvedVersion(resolvedVersion);
3✔
462
    }
463
  }
1✔
464

465
  /**
466
   * Hook for subclasses to adjust the requested tool edition before the version is finalized.
467
   *
468
   * @param requested the requested {@link ToolEditionAndVersion}
469
   * @return the given or trgansformed {@link ToolEditionAndVersion}
470
   */
471
  protected ToolEditionAndVersion adjustRequestedEdition(ToolEditionAndVersion requested) {
472

473
    // default no-op
474
    return requested;
2✔
475
  }
476

477

478
  private void completeRequestToolPath(ToolInstallRequest request) {
479

480
    Path toolPath = request.getToolPath();
3✔
481
    if (toolPath == null) {
2✔
482
      toolPath = getToolPath();
3✔
483
      request.setToolPath(toolPath);
3✔
484
    }
485
  }
1✔
486

487
  /**
488
   * @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
489
   *     determined by the installer.
490
   */
491
  public Path getToolPath() {
492
    return null;
×
493
  }
494

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

502
    if (!request.isAlreadyInstalled()) {
3✔
503
      postInstallOnNewInstallation(request);
3✔
504
    }
505
  }
1✔
506

507
  /**
508
   * This method is called after a tool was requested to be installed or updated and a new installation was performed.
509
   *
510
   * @param request {@code true} the {@link ToolInstallRequest}.
511
   */
512
  protected void postInstallOnNewInstallation(ToolInstallRequest request) {
513

514
    // nothing to do by default
515
  }
1✔
516

517
  /**
518
   * @param edition the {@link #getInstalledEdition() edition}.
519
   * @param version the {@link #getInstalledVersion() version}.
520
   * @return the {@link Path} where this tool is installed (physically) or {@code null} if not available.
521
   */
522
  protected abstract Path getInstallationPath(String edition, VersionIdentifier version);
523

524
  /**
525
   * @param request the {@link ToolInstallRequest}.
526
   * @return the existing {@link ToolInstallation}.
527
   */
528
  protected ToolInstallation createExistingToolInstallation(ToolInstallRequest request) {
529

530
    ToolEditionAndVersion installed = request.getInstalled();
3✔
531

532
    String edition = this.tool;
3✔
533
    VersionIdentifier resolvedVersion = VersionIdentifier.LATEST;
2✔
534

535
    if (installed != null) {
2!
536
      if (installed.getEdition() != null) {
3!
537
        edition = installed.getEdition().edition();
4✔
538
      }
539
      if (installed.getResolvedVersion() != null) {
3!
540
        resolvedVersion = installed.getResolvedVersion();
3✔
541
      }
542
    }
543

544
    return createExistingToolInstallation(edition, resolvedVersion, request.getProcessContext(),
8✔
545
        request.isAdditionalInstallation());
1✔
546
  }
547

548
  /**
549
   * @param edition the {@link #getConfiguredEdition() edition}.
550
   * @param installedVersion the {@link #getConfiguredVersion() version}.
551
   * @param environmentContext the {@link EnvironmentContext}.
552
   * @param extraInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
553
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
554
   * @return the {@link ToolInstallation}.
555
   */
556
  protected ToolInstallation createExistingToolInstallation(String edition, VersionIdentifier installedVersion, EnvironmentContext environmentContext,
557
      boolean extraInstallation) {
558

559
    Path installationPath = getInstallationPath(edition, installedVersion);
5✔
560
    return createToolInstallation(installationPath, installedVersion, false, environmentContext, extraInstallation);
8✔
561
  }
562

563
  /**
564
   * @param rootDir the {@link ToolInstallation#rootDir() top-level installation directory}.
565
   * @param version the installed {@link VersionIdentifier}.
566
   * @param newInstallation {@link ToolInstallation#newInstallation() new installation} flag.
567
   * @param environmentContext the {@link EnvironmentContext}.
568
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
569
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
570
   * @return the {@link ToolInstallation}.
571
   */
572
  protected ToolInstallation createToolInstallation(Path rootDir, VersionIdentifier version, boolean newInstallation,
573
      EnvironmentContext environmentContext, boolean additionalInstallation) {
574

575
    Path linkDir = rootDir;
2✔
576
    Path binDir = rootDir;
2✔
577
    if (rootDir != null) {
2!
578
      // on MacOS applications have a very strange structure - see JavaDoc of findLinkDir and ToolInstallation.linkDir for details.
579
      linkDir = getMacOsHelper().findLinkDir(rootDir, getBinaryName());
7✔
580
      binDir = this.context.getFileAccess().getBinPath(linkDir);
6✔
581
    }
582
    return createToolInstallation(rootDir, linkDir, binDir, version, newInstallation, environmentContext, additionalInstallation);
10✔
583
  }
584

585
  /**
586
   * @param rootDir the {@link ToolInstallation#rootDir() top-level installation directory}.
587
   * @param linkDir the {@link ToolInstallation#linkDir() link directory}.
588
   * @param binDir the {@link ToolInstallation#binDir() bin directory}.
589
   * @param version the installed {@link VersionIdentifier}.
590
   * @param newInstallation {@link ToolInstallation#newInstallation() new installation} flag.
591
   * @param environmentContext the {@link EnvironmentContext}.
592
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
593
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
594
   * @return the {@link ToolInstallation}.
595
   */
596
  protected ToolInstallation createToolInstallation(Path rootDir, Path linkDir, Path binDir, VersionIdentifier version, boolean newInstallation,
597
      EnvironmentContext environmentContext, boolean additionalInstallation) {
598

599
    // do not copy the version file into macOS .app bundles: changing the bundle after codesigning breaks the seal.
600
    ToolInstallation toolInstallation = new ToolInstallation(rootDir, linkDir, binDir, version, newInstallation);
9✔
601
    setEnvironment(environmentContext, toolInstallation, additionalInstallation);
5✔
602
    return toolInstallation;
2✔
603
  }
604

605
  /**
606
   * Called if the tool {@link ToolInstallRequest#isAlreadyInstalled() is already installed in the correct edition and version} so we can skip the
607
   * installation.
608
   *
609
   * @param request the {@link ToolInstallRequest}.
610
   * @return the {@link ToolInstallation}.
611
   */
612
  protected ToolInstallation toolAlreadyInstalled(ToolInstallRequest request) {
613

614
    logToolAlreadyInstalled(request);
3✔
615
    cveCheck(request);
4✔
616
    postInstall(request);
3✔
617
    return createExistingToolInstallation(request);
4✔
618
  }
619

620
  /**
621
   * Log that the tool is already installed.
622
   *
623
   * @param request the {@link ToolInstallRequest}.
624
   */
625
  protected void logToolAlreadyInstalled(ToolInstallRequest request) {
626
    Level level;
627
    if (request.isSilent()) {
3✔
628
      level = Level.DEBUG;
3✔
629
    } else {
630
      level = Level.INFO;
2✔
631
    }
632
    ToolEditionAndVersion installed = request.getInstalled();
3✔
633
    LOG.atLevel(level).log("Version {} of tool {} is already installed", installed.getVersion(), installed.getEdition());
9✔
634
  }
1✔
635

636
  /**
637
   * Method to get the home path of the given {@link ToolInstallation}.
638
   *
639
   * @param toolInstallation the {@link ToolInstallation}.
640
   * @return the Path to the home of the tool
641
   */
642
  protected Path getToolHomePath(ToolInstallation toolInstallation) {
643
    return toolInstallation.linkDir();
3✔
644
  }
645

646
  /**
647
   * Method to set environment variables for the process context.
648
   *
649
   * @param environmentContext the {@link EnvironmentContext} where to {@link EnvironmentContext#withEnvVar(String, String) set environment variables} for
650
   *     this tool.
651
   * @param toolInstallation the {@link ToolInstallation}.
652
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
653
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
654
   */
655
  public void setEnvironment(EnvironmentContext environmentContext, ToolInstallation toolInstallation, boolean additionalInstallation) {
656

657
    String pathVariable = EnvironmentVariables.getToolVariablePrefix(this.tool) + "_HOME";
5✔
658
    Path toolHomePath = getToolHomePath(toolInstallation);
4✔
659
    if (toolHomePath != null) {
2!
660
      environmentContext.withEnvVar(pathVariable, toolHomePath.toString());
6✔
661
    }
662
    if (additionalInstallation) {
2✔
663
      environmentContext.withPathEntry(toolInstallation.binDir());
5✔
664
    }
665
  }
1✔
666

667
  /**
668
   * @return {@code true} to extract (unpack) the downloaded binary file, {@code false} otherwise.
669
   */
670
  protected boolean isExtract() {
671

672
    return true;
2✔
673
  }
674

675
  /**
676
   * 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
677
   * something better, we will suggest this to the user and ask him to make his choice.
678
   *
679
   * @param request the {@link ToolInstallRequest}.
680
   * @return the {@link VersionIdentifier} to install. The will may be asked (unless {@code skipSuggestions} is {@code true}) and might choose a different
681
   *     version than the originally requested one.
682
   */
683
  protected VersionIdentifier cveCheck(ToolInstallRequest request) {
684

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

720
      if (Objects.equals(version, resolvedVersion)) {
4✔
721
        continue; // Skip the entire iteration for resolvedVersion
1✔
722
      }
723

724
      if (acceptVersion(version, allowedVersions, requireStableVersion)) {
5!
725
        ToolVulnerabilities newVulnerabilities = toolSecurity.findCves(version, minSeverity);
5✔
726
        if (newVulnerabilities.isSafer(latestVulnerabilities)) {
4✔
727
          // we found a better/safer version
728
          ToolEditionAndVersion toolEditionAndVersion = new ToolEditionAndVersion(toolEdition, version);
6✔
729
          if (version.isGreater(resolvedVersion)) {
4!
730
            latestVulnerabilities = newVulnerabilities;
2✔
731
            latest = ToolVersionChoice.ofLatest(toolEditionAndVersion, latestVulnerabilities);
4✔
732
            nearest = null;
3✔
733
          } else {
734
            nearestVulnerabilities = newVulnerabilities;
×
735
            nearest = ToolVersionChoice.ofNearest(toolEditionAndVersion, nearestVulnerabilities);
×
736
          }
737
        } else if (newVulnerabilities.isSaferOrEqual(nearestVulnerabilities)) {
5✔
738
          if (newVulnerabilities.isSafer(nearestVulnerabilities) || version.isGreater(resolvedVersion)) {
8!
739
            nearest = ToolVersionChoice.ofNearest(new ToolEditionAndVersion(toolEdition, version), newVulnerabilities);
8✔
740
          }
741
          nearestVulnerabilities = newVulnerabilities;
2✔
742
        }
743
      }
744
    }
1✔
745
    if ((latest == null) && (nearest == null)) {
2!
746
      LOG.warn("Could not find any other version resolving your CVEs.\n"
×
747
          + "Please keep attention to this tool and consider updating as soon as security fixes are available.");
748
      if (alreadyInstalled) {
×
749
        // we came here via "ide -f install ..." but no alternative is available
750
        return resolvedVersion;
×
751
      }
752
    }
753
    List<ToolVersionChoice> choices = new ArrayList<>();
4✔
754
    choices.add(currentChoice);
4✔
755
    boolean addSuggestions;
756
    if (this.context.isForceMode() && request.isDirect()) {
4!
757
      addSuggestions = true;
×
758
    } else {
759
      List<String> skipCveFixTools = IdeVariables.SKIP_CVE_FIX.get(this.context);
6✔
760
      addSuggestions = !skipCveFixTools.contains(this.tool);
8!
761
    }
762
    if (nearest != null) {
2!
763
      if (addSuggestions) {
2!
764
        choices.add(nearest);
4✔
765
      }
766
      nearest.logAndCheckIfEmpty();
3✔
767
    }
768
    if (latest != null) {
2!
769
      if (addSuggestions) {
2!
770
        choices.add(latest);
4✔
771
      }
772
      latest.logAndCheckIfEmpty();
3✔
773
    }
774
    ToolVersionChoice[] choicesArray = choices.toArray(ToolVersionChoice[]::new);
8✔
775
    LOG.warn("Please note that by selecting an unsafe version to install, you accept the risk to be attacked.");
3✔
776
    ToolVersionChoice answer = this.context.question(choicesArray, "Which version do you want to install?");
9✔
777
    VersionIdentifier version = answer.toolEditionAndVersion().getResolvedVersion();
4✔
778
    requested.setResolvedVersion(version);
3✔
779
    return version;
2✔
780
  }
781

782
  private static boolean acceptVersion(VersionIdentifier version, GenericVersionRange allowedVersions, boolean requireStableVersion) {
783
    if (allowedVersions.isPattern() && !allowedVersions.contains(version)) {
3!
784
      return false;
×
785
    } else if (requireStableVersion && !version.isStable()) {
5!
786
      return false;
×
787
    }
788
    return true;
2✔
789
  }
790

791
  /**
792
   * @return the {@link MacOsHelper} instance.
793
   */
794
  protected MacOsHelper getMacOsHelper() {
795

796
    if (this.macOsHelper == null) {
3✔
797
      this.macOsHelper = new MacOsHelper(this.context);
7✔
798
    }
799
    return this.macOsHelper;
3✔
800
  }
801

802
  /**
803
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
804
   */
805
  public abstract VersionIdentifier getInstalledVersion();
806

807
  /**
808
   * @return {@code true} if this tool is installed, {@code false} otherwise.
809
   */
810
  public boolean isInstalled() {
811

812
    return getInstalledVersion() != null;
7✔
813
  }
814

815
  /**
816
   * @return the installed edition of this tool or {@code null} if not installed.
817
   */
818
  public abstract String getInstalledEdition();
819

820
  /**
821
   * Uninstalls the {@link #getName() tool}.
822
   */
823
  public abstract void uninstall();
824

825
  /**
826
   * @return the {@link ToolRepository}.
827
   */
828
  public ToolRepository getToolRepository() {
829

830
    return this.context.getDefaultToolRepository();
4✔
831
  }
832

833
  /**
834
   * List the available editions of this tool.
835
   */
836
  public void listEditions() {
837

838
    List<String> editions = getToolRepository().getSortedEditions(getName());
6✔
839
    for (String edition : editions) {
10✔
840
      LOG.info(edition);
3✔
841
    }
1✔
842
  }
1✔
843

844
  /**
845
   * List the available versions of this tool.
846
   */
847
  public void listVersions() {
848

849
    List<VersionIdentifier> versions = getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
850
    for (VersionIdentifier vi : versions) {
10✔
851
      LOG.info(vi.toString());
4✔
852
    }
1✔
853
  }
1✔
854

855
  /**
856
   * @return the {@link com.devonfw.tools.ide.tool.repository.DefaultToolRepository#getSortedVersions(String, String, ToolCommandlet) sorted versions} of this
857
   *     tool.
858
   */
859
  public List<VersionIdentifier> getVersions() {
860
    return getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
861
  }
862

863
  /**
864
   * Sets the tool version in the environment variable configuration file.
865
   *
866
   * @param version the version (pattern) to set.
867
   */
868
  public void setVersion(String version) {
869

870
    if ((version == null) || version.isBlank()) {
×
871
      throw new IllegalStateException("Version has to be specified!");
×
872
    }
873
    VersionIdentifier configuredVersion = VersionIdentifier.of(version);
×
874
    if (!configuredVersion.isPattern() && !configuredVersion.isValid()) {
×
875
      LOG.warn("Version {} seems to be invalid", version);
×
876
    }
877
    setVersion(configuredVersion, true);
×
878
  }
×
879

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

888
    setVersion(version, hint, null);
5✔
889
  }
1✔
890

891
  /**
892
   * Sets the tool version in the environment variable configuration file.
893
   *
894
   * @param version the version to set. May also be a {@link VersionIdentifier#isPattern() version pattern}.
895
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
896
   * @param destination - the destination for the property to be set
897
   */
898
  public void setVersion(VersionIdentifier version, boolean hint, EnvironmentVariablesFiles destination) {
899

900
    String edition = getConfiguredEdition();
3✔
901
    ToolRepository toolRepository = getToolRepository();
3✔
902

903
    EnvironmentVariables variables = this.context.getVariables();
4✔
904
    if (destination == null) {
2✔
905
      //use default location
906
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
907
    }
908
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
909
    String variableName = EnvironmentVariables.getToolVersionVariable(this.tool);
4✔
910

911
    VersionIdentifier resolvedVersion = toolRepository.resolveVersion(this.tool, edition, version, this); // verify that the version actually exists
8✔
912
    settingsVariables.set(variableName, version.toString(), false);
7✔
913
    settingsVariables.save();
2✔
914
    EnvironmentVariables declaringVariables = variables.findVariable(variableName);
4✔
915
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
916
      LOG.warn("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", variableName,
5✔
917
          declaringVariables.getSource());
1✔
918
    }
919
    LOG.info("Version of tool {} has been set to {} ({}={})", this.tool, version, variableName, version);
22✔
920
    if (hint && !resolvedVersion.equals(getInstalledVersion())) {
7✔
921
      IdeLogLevel.INTERACTION.log(LOG, "To install that version call the following command:\nide install {}", this.tool);
11✔
922
    }
923
  }
1✔
924

925
  /**
926
   * Sets the tool edition in the environment variable configuration file.
927
   *
928
   * @param edition the edition to set.
929
   */
930
  public void setEdition(String edition) {
931

932
    setEdition(edition, true);
4✔
933
  }
1✔
934

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

943
    setEdition(edition, hint, null);
5✔
944
  }
1✔
945

946
  /**
947
   * Sets the tool edition in the environment variable configuration file.
948
   *
949
   * @param edition the edition to set
950
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
951
   * @param destination - the destination for the property to be set
952
   */
953
  public void setEdition(String edition, boolean hint, EnvironmentVariablesFiles destination) {
954

955
    if ((edition == null) || edition.isBlank()) {
5!
956
      throw new IllegalStateException("Edition has to be specified!");
×
957
    }
958

959
    if (destination == null) {
2✔
960
      //use default location
961
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
962
    }
963

964
    if (!getToolRepository().getSortedEditions(this.tool).contains(edition)) {
8✔
965
      LOG.warn("Edition {} seems to be invalid", edition);
4✔
966
    }
967
    EnvironmentVariables variables = this.context.getVariables();
4✔
968
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
969
    String name = EnvironmentVariables.getToolEditionVariable(this.tool);
4✔
970
    settingsVariables.set(name, edition, false);
6✔
971
    settingsVariables.save();
2✔
972

973
    LOG.info("{}={} has been set in {}", name, edition, settingsVariables.getSource());
18✔
974
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
975
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
976
      LOG.warn("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name,
5✔
977
          declaringVariables.getSource());
1✔
978
    }
979
    if (hint) {
2!
980
      LOG.info("To install that edition call the following command:");
3✔
981
      LOG.info("ide install {}", this.tool);
5✔
982
    }
983
  }
1✔
984

985
  /**
986
   * Runs the tool's help command to provide the user with usage information.
987
   */
988
  @Override
989
  public void printHelp(NlsBundle bundle) {
990

991
    super.printHelp(bundle);
3✔
992
    String toolHelpArgs = getToolHelpArguments();
3✔
993
    if (toolHelpArgs != null && getInstalledVersion() != null) {
5!
994
      ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.LOG_WARNING)
6✔
995
          .executable(Path.of(getBinaryName())).addArgs(toolHelpArgs);
13✔
996
      pc.run(ProcessMode.DEFAULT);
4✔
997
    }
998
  }
1✔
999

1000
  /**
1001
   * @return the tool's specific help command. Usually help, --help or -h. Return null if not applicable.
1002
   */
1003
  public String getToolHelpArguments() {
1004

1005
    return null;
×
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
   */
1015
  protected void createStartScript(Path targetDir, String binary) {
1016

1017
    createStartScript(targetDir, binary, false);
×
1018
  }
×
1019

1020
  /**
1021
   * Creates a start script for the tool using the tool name.
1022
   *
1023
   * @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
1024
   *     instead.
1025
   * @param binary name of the binary to execute from the start script.
1026
   * @param background {@code true} to run the {@code binary} in background, {@code false} otherwise (foreground).
1027
   */
1028
  protected void createStartScript(Path targetDir, String binary, boolean background) {
1029

1030
    Path binFolder = targetDir.resolve("bin");
×
1031
    if (!Files.exists(binFolder)) {
×
1032
      if (this.context.getSystemInfo().isMac()) {
×
1033
        MacOsHelper macOsHelper = getMacOsHelper();
×
1034
        Path appDir = macOsHelper.findAppDir(targetDir);
×
1035
        binFolder = macOsHelper.findLinkDir(appDir, binary);
×
1036
      } else {
×
1037
        binFolder = targetDir;
×
1038
      }
1039
      assert (Files.exists(binFolder));
×
1040
    }
1041
    Path bashFile = binFolder.resolve(getName());
×
1042
    String bashFileContentStart = "#!/usr/bin/env bash\n\"$(dirname \"$0\")/";
×
1043
    String bashFileContentEnd = "\" $@";
×
1044
    if (background) {
×
1045
      bashFileContentEnd += " &";
×
1046
    }
1047
    try {
1048
      Files.writeString(bashFile, bashFileContentStart + binary + bashFileContentEnd);
×
1049
    } catch (IOException e) {
×
1050
      throw new RuntimeException(e);
×
1051
    }
×
1052
    assert (Files.exists(bashFile));
×
1053
    context.getFileAccess().makeExecutable(bashFile);
×
1054
  }
×
1055

1056
  @Override
1057
  public void reset() {
1058
    super.reset();
2✔
1059
    this.executionDirectory = null;
3✔
1060
  }
1✔
1061

1062
  /**
1063
   * @param command the binary that will be searched in the PATH e.g. docker
1064
   * @return true if the command is available to use
1065
   */
1066
  protected boolean isCommandAvailable(String command) {
1067
    return this.context.getPath().hasBinaryOnPath(command);
×
1068
  }
1069

1070
  /**
1071
   * @param output the raw output string from executed command e.g. 'docker version'
1072
   * @param pattern Regular Expression pattern that filters out the unnecessary texts.
1073
   * @return version that has been processed.
1074
   */
1075
  protected VersionIdentifier resolveVersionWithPattern(String output, Pattern pattern) {
1076
    Matcher matcher = pattern.matcher(output);
×
1077

1078
    if (matcher.find()) {
×
1079
      return VersionIdentifier.of(matcher.group(1));
×
1080
    } else {
1081
      return null;
×
1082
    }
1083
  }
1084

1085
  /**
1086
   * @deprecated directly log success message and then report success on step if not null.
1087
   */
1088
  @Deprecated
1089
  protected void success(Step step, String message, Object... args) {
1090

1091
    if (step == null) {
×
1092
      IdeLogLevel.SUCCESS.log(LOG, message, args);
×
1093
    } else {
1094
      step.success(message, args);
×
1095
    }
1096
  }
×
1097

1098
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc