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

devonfw / IDEasy / 25508088456

07 May 2026 04:17PM UTC coverage: 70.732% (+0.09%) from 70.647%
25508088456

Pull #1885

github

web-flow
Merge 1a283373a into fd215c395
Pull Request #1885: 1518 uv tools are now installed locally

4401 of 6878 branches covered (63.99%)

Branch coverage included in aggregate %.

11361 of 15406 relevant lines covered (73.74%)

3.12 hits per line

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

73.6
cli/src/main/java/com/devonfw/tools/ide/tool/ToolCommandlet.java
1
package com.devonfw.tools.ide.tool;
2

3
import java.io.IOException;
4
import java.nio.file.Files;
5
import java.nio.file.Path;
6
import java.util.ArrayList;
7
import java.util.List;
8
import java.util.Objects;
9
import java.util.Set;
10
import java.util.regex.Matcher;
11
import java.util.regex.Pattern;
12

13
import org.slf4j.Logger;
14
import org.slf4j.LoggerFactory;
15
import org.slf4j.event.Level;
16

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

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

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

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

53
  private final Set<Tag> tags;
54

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

58
  private Path executionDirectory;
59

60
  private MacOsHelper macOsHelper;
61

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

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

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

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

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

93
    return this.tool;
3✔
94
  }
95

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

101
    return this.tool;
3✔
102
  }
103

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

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

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

120
    return this.tags;
3✔
121
  }
122

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

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

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

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

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

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

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

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

163
  @Override
164
  protected void doRun() {
165

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

331
  private void completeRequestInstalled(ToolInstallRequest request) {
332

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

363
  private void completeRequestRequested(ToolInstallRequest request) {
364

365
    ToolEdition edition;
366
    ToolEditionAndVersion requested = request.getRequested();
3✔
367
    if (requested == null) {
2✔
368
      edition = new ToolEdition(this.tool, getConfiguredEdition());
8✔
369
      requested = new ToolEditionAndVersion(edition);
5✔
370
      request.setRequested(requested);
4✔
371
    
372
    } else {
373
      edition = requested.getEdition();
3✔
374
      if (edition == null) {
2✔
375
        // If no edition was specified, set it to the configured one
376
        edition = new ToolEdition(this.tool, getConfiguredEdition());
8✔
377
        requested.setEdition(edition);
3✔
378
      }
379
    }
380

381
    // Adjust edition if necessary based on requested version. This is needed for tools like IntelliJ where we may need to automatically switch editions
382
    requested = adjustRequestedEdition(requested);
4✔
383
    edition = requested.getEdition();
3✔
384
  
385
    GenericVersionRange version = requested.getVersion();
3✔
386
    if (version == null) {
2✔
387
      version = getConfiguredVersion();
3✔
388
      requested.setVersion(version);
3✔
389
    }
390
    VersionIdentifier resolvedVersion = requested.getResolvedVersion();
3✔
391
    if (resolvedVersion == null) {
2✔
392
      if (this.context.isSkipUpdatesMode()) {
4✔
393
        ToolEditionAndVersion installed = request.getInstalled();
3✔
394
        if (installed != null) {
2!
395
          VersionIdentifier installedVersion = installed.getResolvedVersion();
3✔
396
          if (version.contains(installedVersion)) {
4✔
397
            resolvedVersion = installedVersion;
2✔
398
          }
399
        }
400
      }
401
      if (resolvedVersion == null) {
2✔
402
        resolvedVersion = getToolRepository().resolveVersion(this.tool, edition.edition(), version, this);
10✔
403
      }
404
      requested.setResolvedVersion(resolvedVersion);
3✔
405
    }
406
  }
1✔
407

408
  /**
409
   * Hook for subclasses to adjust the requested tool edition before the version is finalized.
410
   *
411
   * @param requested the requested {@link ToolEditionAndVersion}
412
   * @return the given or trgansformed {@link ToolEditionAndVersion}
413
   */
414
  protected ToolEditionAndVersion adjustRequestedEdition(ToolEditionAndVersion requested) {
415

416
    // default no-op
417
    return requested;
2✔
418
  }
419

420

421
  private void completeRequestToolPath(ToolInstallRequest request) {
422

423
    Path toolPath = request.getToolPath();
3✔
424
    if (toolPath == null) {
2✔
425
      toolPath = getToolPath();
3✔
426
      request.setToolPath(toolPath);
3✔
427
    }
428
  }
1✔
429

430
  /**
431
   * @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
432
   *     determined by the installer.
433
   */
434
  public Path getToolPath() {
435
    return null;
×
436
  }
437

438
  /**
439
   * This method is called after a tool was requested to be installed or updated.
440
   *
441
   * @param request {@code true} the {@link ToolInstallRequest}.
442
   */
443
  protected void postInstall(ToolInstallRequest request) {
444

445
    if (!request.isAlreadyInstalled()) {
3✔
446
      postInstallOnNewInstallation(request);
3✔
447
    }
448
  }
1✔
449

450
  /**
451
   * This method is called after a tool was requested to be installed or updated and a new installation was performed.
452
   *
453
   * @param request {@code true} the {@link ToolInstallRequest}.
454
   */
455
  protected void postInstallOnNewInstallation(ToolInstallRequest request) {
456

457
    // nothing to do by default
458
  }
1✔
459

460
  /**
461
   * @param edition the {@link #getInstalledEdition() edition}.
462
   * @param version the {@link #getInstalledVersion() version}.
463
   * @return the {@link Path} where this tool is installed (physically) or {@code null} if not available.
464
   */
465
  protected abstract Path getInstallationPath(String edition, VersionIdentifier version);
466

467
  /**
468
   * @param request the {@link ToolInstallRequest}.
469
   * @return the existing {@link ToolInstallation}.
470
   */
471
  protected ToolInstallation createExistingToolInstallation(ToolInstallRequest request) {
472

473
    ToolEditionAndVersion installed = request.getInstalled();
3✔
474

475
    String edition = this.tool;
3✔
476
    VersionIdentifier resolvedVersion = VersionIdentifier.LATEST;
2✔
477

478
    if (installed != null) {
2!
479
      if (installed.getEdition() != null) {
3!
480
        edition = installed.getEdition().edition();
4✔
481
      }
482
      if (installed.getResolvedVersion() != null) {
3!
483
        resolvedVersion = installed.getResolvedVersion();
3✔
484
      }
485
    }
486

487
    return createExistingToolInstallation(edition, resolvedVersion, request.getProcessContext(),
8✔
488
        request.isAdditionalInstallation());
1✔
489
  }
490

491
  /**
492
   * @param edition the {@link #getConfiguredEdition() edition}.
493
   * @param installedVersion the {@link #getConfiguredVersion() version}.
494
   * @param environmentContext the {@link EnvironmentContext}.
495
   * @param extraInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
496
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
497
   * @return the {@link ToolInstallation}.
498
   */
499
  protected ToolInstallation createExistingToolInstallation(String edition, VersionIdentifier installedVersion, EnvironmentContext environmentContext,
500
      boolean extraInstallation) {
501

502
    Path installationPath = getInstallationPath(edition, installedVersion);
5✔
503
    return createToolInstallation(installationPath, installedVersion, false, environmentContext, extraInstallation);
8✔
504
  }
505

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

518
    Path linkDir = rootDir;
2✔
519
    Path binDir = rootDir;
2✔
520
    if (rootDir != null) {
2!
521
      // on MacOS applications have a very strange structure - see JavaDoc of findLinkDir and ToolInstallation.linkDir for details.
522
      linkDir = getMacOsHelper().findLinkDir(rootDir, getBinaryName());
7✔
523
      binDir = this.context.getFileAccess().getBinPath(linkDir);
6✔
524
    }
525
    return createToolInstallation(rootDir, linkDir, binDir, version, newInstallation, environmentContext, additionalInstallation);
10✔
526
  }
527

528
  /**
529
   * @param rootDir the {@link ToolInstallation#rootDir() top-level installation directory}.
530
   * @param linkDir the {@link ToolInstallation#linkDir() link directory}.
531
   * @param binDir the {@link ToolInstallation#binDir() bin directory}.
532
   * @param version the installed {@link VersionIdentifier}.
533
   * @param newInstallation {@link ToolInstallation#newInstallation() new installation} flag.
534
   * @param environmentContext the {@link EnvironmentContext}.
535
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
536
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
537
   * @return the {@link ToolInstallation}.
538
   */
539
  protected ToolInstallation createToolInstallation(Path rootDir, Path linkDir, Path binDir, VersionIdentifier version, boolean newInstallation,
540
      EnvironmentContext environmentContext, boolean additionalInstallation) {
541

542
    // do not copy the version file into macOS .app bundles: changing the bundle after codesigning breaks the seal.
543
    ToolInstallation toolInstallation = new ToolInstallation(rootDir, linkDir, binDir, version, newInstallation);
9✔
544
    setEnvironment(environmentContext, toolInstallation, additionalInstallation);
5✔
545
    return toolInstallation;
2✔
546
  }
547

548
  /**
549
   * Called if the tool {@link ToolInstallRequest#isAlreadyInstalled() is already installed in the correct edition and version} so we can skip the
550
   * installation.
551
   *
552
   * @param request the {@link ToolInstallRequest}.
553
   * @return the {@link ToolInstallation}.
554
   */
555
  protected ToolInstallation toolAlreadyInstalled(ToolInstallRequest request) {
556

557
    logToolAlreadyInstalled(request);
3✔
558
    cveCheck(request);
4✔
559
    postInstall(request);
3✔
560
    return createExistingToolInstallation(request);
4✔
561
  }
562

563
  /**
564
   * Log that the tool is already installed.
565
   *
566
   * @param request the {@link ToolInstallRequest}.
567
   */
568
  protected void logToolAlreadyInstalled(ToolInstallRequest request) {
569
    Level level;
570
    if (request.isSilent()) {
3✔
571
      level = Level.DEBUG;
3✔
572
    } else {
573
      level = Level.INFO;
2✔
574
    }
575
    ToolEditionAndVersion installed = request.getInstalled();
3✔
576
    LOG.atLevel(level).log("Version {} of tool {} is already installed", installed.getVersion(), installed.getEdition());
9✔
577
  }
1✔
578

579
  /**
580
   * Method to get the home path of the given {@link ToolInstallation}.
581
   *
582
   * @param toolInstallation the {@link ToolInstallation}.
583
   * @return the Path to the home of the tool
584
   */
585
  protected Path getToolHomePath(ToolInstallation toolInstallation) {
586
    return toolInstallation.linkDir();
3✔
587
  }
588

589
  /**
590
   * Method to set environment variables for the process context.
591
   *
592
   * @param environmentContext the {@link EnvironmentContext} where to {@link EnvironmentContext#withEnvVar(String, String) set environment variables} for
593
   *     this tool.
594
   * @param toolInstallation the {@link ToolInstallation}.
595
   * @param additionalInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
596
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
597
   */
598
  public void setEnvironment(EnvironmentContext environmentContext, ToolInstallation toolInstallation, boolean additionalInstallation) {
599

600
    String pathVariable = EnvironmentVariables.getToolVariablePrefix(this.tool) + "_HOME";
5✔
601
    Path toolHomePath = getToolHomePath(toolInstallation);
4✔
602
    if (toolHomePath != null) {
2!
603
      environmentContext.withEnvVar(pathVariable, toolHomePath.toString());
6✔
604
    }
605
    if (additionalInstallation) {
2✔
606
      environmentContext.withPathEntry(toolInstallation.binDir());
5✔
607
    }
608
  }
1✔
609

610
  /**
611
   * @return {@code true} to extract (unpack) the downloaded binary file, {@code false} otherwise.
612
   */
613
  protected boolean isExtract() {
614

615
    return true;
2✔
616
  }
617

618
  /**
619
   * 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
620
   * something better, we will suggest this to the user and ask him to make his choice.
621
   *
622
   * @param request the {@link ToolInstallRequest}.
623
   * @return the {@link VersionIdentifier} to install. The will may be asked (unless {@code skipSuggestions} is {@code true}) and might choose a different
624
   *     version than the originally requested one.
625
   */
626
  protected VersionIdentifier cveCheck(ToolInstallRequest request) {
627

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

663
      if (Objects.equals(version, resolvedVersion)) {
4✔
664
        continue; // Skip the entire iteration for resolvedVersion
1✔
665
      }
666

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

725
  private static boolean acceptVersion(VersionIdentifier version, GenericVersionRange allowedVersions, boolean requireStableVersion) {
726
    if (allowedVersions.isPattern() && !allowedVersions.contains(version)) {
3!
727
      return false;
×
728
    } else if (requireStableVersion && !version.isStable()) {
5!
729
      return false;
×
730
    }
731
    return true;
2✔
732
  }
733

734
  /**
735
   * @return the {@link MacOsHelper} instance.
736
   */
737
  protected MacOsHelper getMacOsHelper() {
738

739
    if (this.macOsHelper == null) {
3✔
740
      this.macOsHelper = new MacOsHelper(this.context);
7✔
741
    }
742
    return this.macOsHelper;
3✔
743
  }
744

745
  /**
746
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
747
   */
748
  public abstract VersionIdentifier getInstalledVersion();
749

750
  /**
751
   * @return {@code true} if this tool is installed, {@code false} otherwise.
752
   */
753
  public boolean isInstalled() {
754

755
    return getInstalledVersion() != null;
7✔
756
  }
757

758
  /**
759
   * @return the installed edition of this tool or {@code null} if not installed.
760
   */
761
  public abstract String getInstalledEdition();
762

763
  /**
764
   * Uninstalls the {@link #getName() tool}.
765
   */
766
  public abstract void uninstall();
767

768
  /**
769
   * @return the {@link ToolRepository}.
770
   */
771
  public ToolRepository getToolRepository() {
772

773
    return this.context.getDefaultToolRepository();
4✔
774
  }
775

776
  /**
777
   * List the available editions of this tool.
778
   */
779
  public void listEditions() {
780

781
    List<String> editions = getToolRepository().getSortedEditions(getName());
6✔
782
    for (String edition : editions) {
10✔
783
      LOG.info(edition);
3✔
784
    }
1✔
785
  }
1✔
786

787
  /**
788
   * List the available versions of this tool.
789
   */
790
  public void listVersions() {
791

792
    List<VersionIdentifier> versions = getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
793
    for (VersionIdentifier vi : versions) {
10✔
794
      LOG.info(vi.toString());
4✔
795
    }
1✔
796
  }
1✔
797

798
  /**
799
   * @return the {@link com.devonfw.tools.ide.tool.repository.DefaultToolRepository#getSortedVersions(String, String, ToolCommandlet) sorted versions} of this
800
   *     tool.
801
   */
802
  public List<VersionIdentifier> getVersions() {
803
    return getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
804
  }
805

806
  /**
807
   * Sets the tool version in the environment variable configuration file.
808
   *
809
   * @param version the version (pattern) to set.
810
   */
811
  public void setVersion(String version) {
812

813
    if ((version == null) || version.isBlank()) {
×
814
      throw new IllegalStateException("Version has to be specified!");
×
815
    }
816
    VersionIdentifier configuredVersion = VersionIdentifier.of(version);
×
817
    if (!configuredVersion.isPattern() && !configuredVersion.isValid()) {
×
818
      LOG.warn("Version {} seems to be invalid", version);
×
819
    }
820
    setVersion(configuredVersion, true);
×
821
  }
×
822

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

831
    setVersion(version, hint, null);
5✔
832
  }
1✔
833

834
  /**
835
   * Sets the tool version in the environment variable configuration file.
836
   *
837
   * @param version the version to set. May also be a {@link VersionIdentifier#isPattern() version pattern}.
838
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
839
   * @param destination - the destination for the property to be set
840
   */
841
  public void setVersion(VersionIdentifier version, boolean hint, EnvironmentVariablesFiles destination) {
842

843
    String edition = getConfiguredEdition();
3✔
844
    ToolRepository toolRepository = getToolRepository();
3✔
845

846
    EnvironmentVariables variables = this.context.getVariables();
4✔
847
    if (destination == null) {
2✔
848
      //use default location
849
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
850
    }
851
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
852
    String name = EnvironmentVariables.getToolVersionVariable(this.tool);
4✔
853

854
    toolRepository.resolveVersion(this.tool, edition, version, this); // verify that the version actually exists
8✔
855
    settingsVariables.set(name, version.toString(), false);
7✔
856
    settingsVariables.save();
2✔
857
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
858
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
859
      LOG.warn("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name,
5✔
860
          declaringVariables.getSource());
1✔
861
    }
862
    if (hint) {
2✔
863
      LOG.info("To install that version call the following command:");
3✔
864
      LOG.info("ide install {}", this.tool);
5✔
865
    }
866
  }
1✔
867

868
  /**
869
   * Sets the tool edition in the environment variable configuration file.
870
   *
871
   * @param edition the edition to set.
872
   */
873
  public void setEdition(String edition) {
874

875
    setEdition(edition, true);
4✔
876
  }
1✔
877

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

886
    setEdition(edition, hint, null);
5✔
887
  }
1✔
888

889
  /**
890
   * Sets the tool edition in the environment variable configuration file.
891
   *
892
   * @param edition the edition to set
893
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
894
   * @param destination - the destination for the property to be set
895
   */
896
  public void setEdition(String edition, boolean hint, EnvironmentVariablesFiles destination) {
897

898
    if ((edition == null) || edition.isBlank()) {
5!
899
      throw new IllegalStateException("Edition has to be specified!");
×
900
    }
901

902
    if (destination == null) {
2✔
903
      //use default location
904
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
905
    }
906

907
    if (!getToolRepository().getSortedEditions(this.tool).contains(edition)) {
8✔
908
      LOG.warn("Edition {} seems to be invalid", edition);
4✔
909
    }
910
    EnvironmentVariables variables = this.context.getVariables();
4✔
911
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
912
    String name = EnvironmentVariables.getToolEditionVariable(this.tool);
4✔
913
    settingsVariables.set(name, edition, false);
6✔
914
    settingsVariables.save();
2✔
915

916
    LOG.info("{}={} has been set in {}", name, edition, settingsVariables.getSource());
18✔
917
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
918
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
919
      LOG.warn("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name,
5✔
920
          declaringVariables.getSource());
1✔
921
    }
922
    if (hint) {
2!
923
      LOG.info("To install that edition call the following command:");
3✔
924
      LOG.info("ide install {}", this.tool);
5✔
925
    }
926
  }
1✔
927

928
  /**
929
   * Runs the tool's help command to provide the user with usage information.
930
   */
931
  @Override
932
  public void printHelp(NlsBundle bundle) {
933

934
    super.printHelp(bundle);
3✔
935
    String toolHelpArgs = getToolHelpArguments();
3✔
936
    if (toolHelpArgs != null && getInstalledVersion() != null) {
5!
937
      ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.LOG_WARNING)
6✔
938
          .executable(Path.of(getBinaryName())).addArgs(toolHelpArgs);
13✔
939
      pc.run(ProcessMode.DEFAULT);
4✔
940
    }
941
  }
1✔
942

943
  /**
944
   * @return the tool's specific help command. Usually help, --help or -h. Return null if not applicable.
945
   */
946
  public String getToolHelpArguments() {
947

948
    return null;
×
949
  }
950

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

960
    createStartScript(targetDir, binary, false);
×
961
  }
×
962

963
  /**
964
   * Creates a start script for the tool using the tool name.
965
   *
966
   * @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
967
   *     instead.
968
   * @param binary name of the binary to execute from the start script.
969
   * @param background {@code true} to run the {@code binary} in background, {@code false} otherwise (foreground).
970
   */
971
  protected void createStartScript(Path targetDir, String binary, boolean background) {
972

973
    Path binFolder = targetDir.resolve("bin");
×
974
    if (!Files.exists(binFolder)) {
×
975
      if (this.context.getSystemInfo().isMac()) {
×
976
        MacOsHelper macOsHelper = getMacOsHelper();
×
977
        Path appDir = macOsHelper.findAppDir(targetDir);
×
978
        binFolder = macOsHelper.findLinkDir(appDir, binary);
×
979
      } else {
×
980
        binFolder = targetDir;
×
981
      }
982
      assert (Files.exists(binFolder));
×
983
    }
984
    Path bashFile = binFolder.resolve(getName());
×
985
    String bashFileContentStart = "#!/usr/bin/env bash\n\"$(dirname \"$0\")/";
×
986
    String bashFileContentEnd = "\" $@";
×
987
    if (background) {
×
988
      bashFileContentEnd += " &";
×
989
    }
990
    try {
991
      Files.writeString(bashFile, bashFileContentStart + binary + bashFileContentEnd);
×
992
    } catch (IOException e) {
×
993
      throw new RuntimeException(e);
×
994
    }
×
995
    assert (Files.exists(bashFile));
×
996
    context.getFileAccess().makeExecutable(bashFile);
×
997
  }
×
998

999
  @Override
1000
  public void reset() {
1001
    super.reset();
2✔
1002
    this.executionDirectory = null;
3✔
1003
  }
1✔
1004

1005
  /**
1006
   * @param command the binary that will be searched in the PATH e.g. docker
1007
   * @return true if the command is available to use
1008
   */
1009
  protected boolean isCommandAvailable(String command) {
1010
    return this.context.getPath().hasBinaryOnPath(command);
×
1011
  }
1012

1013
  /**
1014
   * @param output the raw output string from executed command e.g. 'docker version'
1015
   * @param pattern Regular Expression pattern that filters out the unnecessary texts.
1016
   * @return version that has been processed.
1017
   */
1018
  protected VersionIdentifier resolveVersionWithPattern(String output, Pattern pattern) {
1019
    Matcher matcher = pattern.matcher(output);
×
1020

1021
    if (matcher.find()) {
×
1022
      return VersionIdentifier.of(matcher.group(1));
×
1023
    } else {
1024
      return null;
×
1025
    }
1026
  }
1027

1028
  /**
1029
   * @deprecated directly log success message and then report success on step if not null.
1030
   */
1031
  @Deprecated
1032
  protected void success(Step step, String message, Object... args) {
1033

1034
    if (step == null) {
×
1035
      IdeLogLevel.SUCCESS.log(LOG, message, args);
×
1036
    } else {
1037
      step.success(message, args);
×
1038
    }
1039
  }
×
1040

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