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

devonfw / IDEasy / 17659177311

11 Sep 2025 10:43PM UTC coverage: 68.76% (+0.04%) from 68.725%
17659177311

Pull #1280

github

web-flow
Merge 1ee3ae544 into a2fce66c4
Pull Request #1280: CVE: adapt IDEasy to consider security.json files to warn user

3417 of 5443 branches covered (62.78%)

Branch coverage included in aggregate %.

8920 of 12499 relevant lines covered (71.37%)

3.14 hits per line

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

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

3
import java.io.IOException;
4
import java.nio.file.Files;
5
import java.nio.file.Path;
6
import java.util.List;
7
import java.util.Objects;
8
import java.util.Set;
9

10
import com.devonfw.tools.ide.commandlet.Commandlet;
11
import com.devonfw.tools.ide.common.Tag;
12
import com.devonfw.tools.ide.common.Tags;
13
import com.devonfw.tools.ide.context.IdeContext;
14
import com.devonfw.tools.ide.environment.EnvironmentVariables;
15
import com.devonfw.tools.ide.environment.EnvironmentVariablesFiles;
16
import com.devonfw.tools.ide.log.IdeSubLogger;
17
import com.devonfw.tools.ide.nls.NlsBundle;
18
import com.devonfw.tools.ide.os.MacOsHelper;
19
import com.devonfw.tools.ide.process.EnvironmentContext;
20
import com.devonfw.tools.ide.process.ProcessContext;
21
import com.devonfw.tools.ide.process.ProcessErrorHandling;
22
import com.devonfw.tools.ide.process.ProcessMode;
23
import com.devonfw.tools.ide.process.ProcessResult;
24
import com.devonfw.tools.ide.property.StringProperty;
25
import com.devonfw.tools.ide.step.Step;
26
import com.devonfw.tools.ide.tool.repository.ToolRepository;
27
import com.devonfw.tools.ide.version.GenericVersionRange;
28
import com.devonfw.tools.ide.version.VersionIdentifier;
29

30
/**
31
 * {@link Commandlet} for a tool integrated into the IDE.
32
 */
33
public abstract class ToolCommandlet extends Commandlet implements Tags {
1✔
34

35
  /** @see #getName() */
36
  protected final String tool;
37

38
  private final Set<Tag> tags;
39

40
  /** The commandline arguments to pass to the tool. */
41
  public final StringProperty arguments;
42

43
  private Path executionDirectory;
44

45
  private MacOsHelper macOsHelper;
46

47
  /**
48
   * The constructor.
49
   *
50
   * @param context the {@link IdeContext}.
51
   * @param tool the {@link #getName() tool name}.
52
   * @param tags the {@link #getTags() tags} classifying the tool. Should be created via {@link Set#of(Object) Set.of} method.
53
   */
54
  public ToolCommandlet(IdeContext context, String tool, Set<Tag> tags) {
55

56
    super(context);
3✔
57
    this.tool = tool;
3✔
58
    this.tags = tags;
3✔
59
    addKeyword(tool);
3✔
60
    this.arguments = new StringProperty("", false, true, "args");
9✔
61
    initProperties();
2✔
62
  }
1✔
63

64
  /**
65
   * Add initial Properties to the tool
66
   */
67
  protected void initProperties() {
68

69
    add(this.arguments);
5✔
70
  }
1✔
71

72
  /**
73
   * @return the name of the tool (e.g. "java", "mvn", "npm", "node").
74
   */
75
  @Override
76
  public final String getName() {
77

78
    return this.tool;
3✔
79
  }
80

81
  /**
82
   * @return the name of the binary executable for this tool.
83
   */
84
  protected String getBinaryName() {
85

86
    return this.tool;
3✔
87
  }
88

89
  @Override
90
  public final Set<Tag> getTags() {
91

92
    return this.tags;
3✔
93
  }
94

95
  /**
96
   * @return the execution directory where the tool will be executed. Will be {@code null} by default leading to execution in the users current working
97
   *     directory where IDEasy was called.
98
   * @see #setExecutionDirectory(Path)
99
   */
100
  public Path getExecutionDirectory() {
101
    return this.executionDirectory;
×
102
  }
103

104
  /**
105
   * @param executionDirectory the new value of {@link #getExecutionDirectory()}.
106
   */
107
  public void setExecutionDirectory(Path executionDirectory) {
108
    this.executionDirectory = executionDirectory;
×
109
  }
×
110

111
  /**
112
   * @return the {@link EnvironmentVariables#getToolVersion(String) tool version}.
113
   */
114
  public VersionIdentifier getConfiguredVersion() {
115

116
    return this.context.getVariables().getToolVersion(getName());
7✔
117
  }
118

119
  /**
120
   * @return the {@link EnvironmentVariables#getToolEdition(String) tool edition}.
121
   */
122
  public String getConfiguredEdition() {
123

124
    return this.context.getVariables().getToolEdition(getName());
7✔
125
  }
126

127
  /**
128
   * @return the {@link #getName() tool} with its {@link #getConfiguredEdition() edition}. The edition will be omitted if same as tool.
129
   * @see #getToolWithEdition(String, String)
130
   */
131
  protected final String getToolWithEdition() {
132

133
    return getToolWithEdition(getName(), getConfiguredEdition());
6✔
134
  }
135

136
  /**
137
   * @param tool the tool name.
138
   * @param edition the edition.
139
   * @return the {@link #getName() tool} with its {@link #getConfiguredEdition() edition}. The edition will be omitted if same as tool.
140
   */
141
  protected static String getToolWithEdition(String tool, String edition) {
142

143
    if (tool.equals(edition)) {
4✔
144
      return tool;
2✔
145
    }
146
    return tool + "/" + edition;
4✔
147
  }
148

149
  @Override
150
  public void run() {
151

152
    runTool(this.arguments.asArray());
5✔
153
  }
1✔
154

155
  /**
156
   * @param args the command-line arguments to run the tool.
157
   * @see ToolCommandlet#runTool(ProcessMode, GenericVersionRange, String...)
158
   */
159
  public void runTool(String... args) {
160

161
    runTool(ProcessMode.DEFAULT, null, args);
5✔
162
  }
1✔
163

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

174
    runTool(processMode, toolVersion, ProcessErrorHandling.THROW_CLI, args);
7✔
175
  }
1✔
176

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

189
    ProcessContext pc = this.context.newProcess().errorHandling(errorHandling);
6✔
190
    install(true, pc, null);
6✔
191
    return runTool(processMode, errorHandling, pc, args);
7✔
192
  }
193

194
  /**
195
   * @param processMode the {@link ProcessMode}. Should typically be {@link ProcessMode#DEFAULT} or {@link ProcessMode#BACKGROUND}.
196
   * @param errorHandling the {@link ProcessErrorHandling}.
197
   * @param pc the {@link ProcessContext}.
198
   * @param args the command-line arguments to run the tool.
199
   * @return the {@link ProcessResult result}.
200
   */
201
  public ProcessResult runTool(ProcessMode processMode, ProcessErrorHandling errorHandling, ProcessContext pc, String... args) {
202

203
    if (this.executionDirectory != null) {
3!
204
      pc.directory(this.executionDirectory);
×
205
    }
206
    configureToolBinary(pc, processMode, errorHandling);
5✔
207
    configureToolArgs(pc, processMode, errorHandling, args);
6✔
208
    return pc.run(processMode);
4✔
209
  }
210

211
  /**
212
   * @param pc the {@link ProcessContext}.
213
   * @param processMode the {@link ProcessMode}.
214
   * @param errorHandling the {@link ProcessErrorHandling}.
215
   */
216
  protected void configureToolBinary(ProcessContext pc, ProcessMode processMode, ProcessErrorHandling errorHandling) {
217

218
    pc.executable(Path.of(getBinaryName()));
8✔
219
  }
1✔
220

221
  /**
222
   * @param pc the {@link ProcessContext}.
223
   * @param processMode the {@link ProcessMode}.
224
   * @param errorHandling the {@link ProcessErrorHandling}.
225
   * @param args the command-line arguments to {@link ProcessContext#addArgs(Object...) add}.
226
   */
227
  protected void configureToolArgs(ProcessContext pc, ProcessMode processMode, ProcessErrorHandling errorHandling, String... args) {
228

229
    pc.addArgs(args);
4✔
230
  }
1✔
231

232
  /**
233
   * Creates a new {@link ProcessContext} from the given executable with the provided arguments attached.
234
   *
235
   * @param binaryPath path to the binary executable for this process
236
   * @param args the command-line arguments for this process
237
   * @return {@link ProcessContext}
238
   */
239
  protected ProcessContext createProcessContext(Path binaryPath, String... args) {
240

241
    return this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_ERR).executable(binaryPath).addArgs(args);
×
242
  }
243

244
  /**
245
   * Installs or updates the managed {@link #getName() tool}.
246
   *
247
   * @return {@code true} if the tool was newly installed, {@code false} if the tool was already installed before and nothing has changed.
248
   */
249
  public boolean install() {
250

251
    return install(true);
4✔
252
  }
253

254
  /**
255
   * Performs the installation of the {@link #getName() tool} managed by this {@link com.devonfw.tools.ide.commandlet.Commandlet}.
256
   *
257
   * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
258
   * @return {@code true} if the tool was newly installed, {@code false} if the tool was already installed before and nothing has changed.
259
   */
260
  public boolean install(boolean silent) {
261
    ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_CLI);
6✔
262
    return install(silent, pc, null);
6✔
263
  }
264

265
  /**
266
   * Installs or updates the managed {@link #getName() tool}.
267
   *
268
   * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
269
   * @param processContext the {@link ProcessContext} used to
270
   *     {@link LocalToolCommandlet#setEnvironment(EnvironmentContext, ToolInstallation, boolean) configure environment variables}.
271
   * @param step the {@link Step} to track the installation. May be {@code null} to fail with {@link Exception} on error.
272
   * @return {@code true} if the tool was newly installed, {@code false} if the tool was already installed before and nothing has changed.
273
   */
274
  public abstract boolean install(boolean silent, ProcessContext processContext, Step step);
275

276
  /**
277
   * @return {@code true} to extract (unpack) the downloaded binary file, {@code false} otherwise.
278
   */
279
  protected boolean isExtract() {
280

281
    return true;
2✔
282
  }
283

284
  /**
285
   * @return the {@link MacOsHelper} instance.
286
   */
287
  protected MacOsHelper getMacOsHelper() {
288

289
    if (this.macOsHelper == null) {
3✔
290
      this.macOsHelper = new MacOsHelper(this.context);
7✔
291
    }
292
    return this.macOsHelper;
3✔
293
  }
294

295
  /**
296
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
297
   */
298
  public abstract VersionIdentifier getInstalledVersion();
299

300
  /**
301
   * @return the installed edition of this tool or {@code null} if not installed.
302
   */
303
  public abstract String getInstalledEdition();
304

305
  /**
306
   * Uninstalls the {@link #getName() tool}.
307
   */
308
  public abstract void uninstall();
309

310
  /**
311
   * @return the {@link ToolRepository}.
312
   */
313
  public ToolRepository getToolRepository() {
314

315
    return this.context.getDefaultToolRepository();
4✔
316
  }
317

318
  /**
319
   * List the available editions of this tool.
320
   */
321
  public void listEditions() {
322

323
    List<String> editions = getToolRepository().getSortedEditions(getName());
6✔
324
    for (String edition : editions) {
10✔
325
      this.context.info(edition);
4✔
326
    }
1✔
327
  }
1✔
328

329
  /**
330
   * List the available versions of this tool.
331
   */
332
  public void listVersions() {
333

334
    List<VersionIdentifier> versions = getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
335
    for (VersionIdentifier vi : versions) {
10✔
336
      this.context.info(vi.toString());
5✔
337
    }
1✔
338
  }
1✔
339

340
  /**
341
   * @return the {@link com.devonfw.tools.ide.tool.repository.DefaultToolRepository#getSortedVersions(String, String, ToolCommandlet) sorted versions} of this
342
   *     tool.
343
   */
344
  public List<VersionIdentifier> getVersions() {
345
    return getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
346
  }
347

348
  /**
349
   * Sets the tool version in the environment variable configuration file.
350
   *
351
   * @param version the version (pattern) to set.
352
   */
353
  public void setVersion(String version) {
354

355
    if ((version == null) || version.isBlank()) {
×
356
      throw new IllegalStateException("Version has to be specified!");
×
357
    }
358
    VersionIdentifier configuredVersion = VersionIdentifier.of(version);
×
359
    if (!configuredVersion.isPattern() && !configuredVersion.isValid()) {
×
360
      this.context.warning("Version {} seems to be invalid", version);
×
361
    }
362
    setVersion(configuredVersion, true);
×
363
  }
×
364

365
  /**
366
   * Sets the tool version in the environment variable configuration file.
367
   *
368
   * @param version the version to set. May also be a {@link VersionIdentifier#isPattern() version pattern}.
369
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
370
   */
371
  public void setVersion(VersionIdentifier version, boolean hint) {
372

373
    setVersion(version, hint, null);
5✔
374
  }
1✔
375

376
  /**
377
   * Sets the tool version in the environment variable configuration file.
378
   *
379
   * @param version the version to set. May also be a {@link VersionIdentifier#isPattern() version pattern}.
380
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
381
   * @param destination - the destination for the property to be set
382
   */
383
  public void setVersion(VersionIdentifier version, boolean hint, EnvironmentVariablesFiles destination) {
384

385
    String edition = getConfiguredEdition();
3✔
386
    ToolRepository toolRepository = getToolRepository();
3✔
387
    VersionIdentifier versionIdentifier = toolRepository.resolveVersion(this.tool, edition, version, this);
8✔
388
    Objects.requireNonNull(versionIdentifier);
3✔
389

390
    EnvironmentVariables variables = this.context.getVariables();
4✔
391
    if (destination == null) {
2✔
392
      //use default location
393
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
394
    }
395
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
396
    String name = EnvironmentVariables.getToolVersionVariable(this.tool);
4✔
397

398
    VersionIdentifier resolvedVersion = toolRepository.resolveVersion(this.tool, edition, version, this);
8✔
399
    if (version.isPattern()) {
3!
400
      this.context.debug("Resolved version {} to {} for tool {}/{}", version, resolvedVersion, this.tool, edition);
×
401
    }
402
    settingsVariables.set(name, resolvedVersion.toString(), false);
7✔
403
    settingsVariables.save();
2✔
404
    this.context.info("{}={} has been set in {}", name, version, settingsVariables.getSource());
19✔
405
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
406
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
407
      this.context.warning("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name,
13✔
408
          declaringVariables.getSource());
2✔
409
    }
410
    if (hint) {
2✔
411
      this.context.info("To install that version call the following command:");
4✔
412
      this.context.info("ide install {}", this.tool);
11✔
413
    }
414
  }
1✔
415

416
  /**
417
   * Sets the tool edition in the environment variable configuration file.
418
   *
419
   * @param edition the edition to set.
420
   */
421
  public void setEdition(String edition) {
422

423
    setEdition(edition, true);
4✔
424
  }
1✔
425

426
  /**
427
   * Sets the tool edition in the environment variable configuration file.
428
   *
429
   * @param edition the edition to set
430
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
431
   */
432
  public void setEdition(String edition, boolean hint) {
433

434
    setEdition(edition, hint, null);
5✔
435
  }
1✔
436

437
  /**
438
   * Sets the tool edition in the environment variable configuration file.
439
   *
440
   * @param edition the edition to set
441
   * @param hint - {@code true} to print the installation hint, {@code false} otherwise.
442
   * @param destination - the destination for the property to be set
443
   */
444
  public void setEdition(String edition, boolean hint, EnvironmentVariablesFiles destination) {
445

446
    if ((edition == null) || edition.isBlank()) {
5!
447
      throw new IllegalStateException("Edition has to be specified!");
×
448
    }
449

450
    if (destination == null) {
2✔
451
      //use default location
452
      destination = EnvironmentVariablesFiles.SETTINGS;
2✔
453
    }
454

455
    if (!getToolRepository().getSortedEditions(this.tool).contains(edition)) {
8✔
456
      this.context.warning("Edition {} seems to be invalid", edition);
10✔
457
    }
458
    EnvironmentVariables variables = this.context.getVariables();
4✔
459
    EnvironmentVariables settingsVariables = variables.getByType(destination.toType());
5✔
460
    String name = EnvironmentVariables.getToolEditionVariable(this.tool);
4✔
461
    settingsVariables.set(name, edition, false);
6✔
462
    settingsVariables.save();
2✔
463

464
    this.context.info("{}={} has been set in {}", name, edition, settingsVariables.getSource());
19✔
465
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
466
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
467
      this.context.warning("The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.", name,
13✔
468
          declaringVariables.getSource());
2✔
469
    }
470
    if (hint) {
2!
471
      this.context.info("To install that edition call the following command:");
4✔
472
      this.context.info("ide install {}", this.tool);
11✔
473
    }
474
  }
1✔
475

476
  /**
477
   * Runs the tool's help command to provide the user with usage information.
478
   */
479
  @Override
480
  public void printHelp(NlsBundle bundle) {
481

482
    super.printHelp(bundle);
3✔
483
    String toolHelpArgs = getToolHelpArguments();
3✔
484
    if (toolHelpArgs != null && getInstalledVersion() != null) {
5!
485
      ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.LOG_WARNING)
6✔
486
          .executable(Path.of(getBinaryName())).addArgs(toolHelpArgs);
13✔
487
      pc.run(ProcessMode.DEFAULT);
4✔
488
    }
489
  }
1✔
490

491
  /**
492
   * @return the tool's specific help command. Usually help, --help or -h. Return null if not applicable.
493
   */
494
  public String getToolHelpArguments() {
495

496
    return null;
×
497
  }
498

499
  /**
500
   * Creates a start script for the tool using the tool name.
501
   *
502
   * @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
503
   *     instead.
504
   * @param binary name of the binary to execute from the start script.
505
   */
506
  protected void createStartScript(Path targetDir, String binary) {
507

508
    createStartScript(targetDir, binary, false);
×
509
  }
×
510

511
  /**
512
   * Creates a start script for the tool using the tool name.
513
   *
514
   * @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
515
   *     instead.
516
   * @param binary name of the binary to execute from the start script.
517
   * @param background {@code true} to run the {@code binary} in background, {@code false} otherwise (foreground).
518
   */
519
  protected void createStartScript(Path targetDir, String binary, boolean background) {
520

521
    Path binFolder = targetDir.resolve("bin");
×
522
    if (!Files.exists(binFolder)) {
×
523
      if (this.context.getSystemInfo().isMac()) {
×
524
        MacOsHelper macOsHelper = getMacOsHelper();
×
525
        Path appDir = macOsHelper.findAppDir(targetDir);
×
526
        binFolder = macOsHelper.findLinkDir(appDir, binary);
×
527
      } else {
×
528
        binFolder = targetDir;
×
529
      }
530
      assert (Files.exists(binFolder));
×
531
    }
532
    Path bashFile = binFolder.resolve(getName());
×
533
    String bashFileContentStart = "#!/usr/bin/env bash\n\"$(dirname \"$0\")/";
×
534
    String bashFileContentEnd = "\" $@";
×
535
    if (background) {
×
536
      bashFileContentEnd += " &";
×
537
    }
538
    try {
539
      Files.writeString(bashFile, bashFileContentStart + binary + bashFileContentEnd);
×
540
    } catch (IOException e) {
×
541
      throw new RuntimeException(e);
×
542
    }
×
543
    assert (Files.exists(bashFile));
×
544
    context.getFileAccess().makeExecutable(bashFile);
×
545
  }
×
546

547
  @Override
548
  public void reset() {
549
    super.reset();
2✔
550
    this.executionDirectory = null;
3✔
551
  }
1✔
552

553
  /**
554
   * @param step the {@link Step} to get {@link Step#asSuccess() success logger} from. May be {@code null}.
555
   * @return the {@link IdeSubLogger} from {@link Step#asSuccess()} or {@link IdeContext#success()} as fallback.
556
   */
557
  protected IdeSubLogger asSuccess(Step step) {
558

559
    if (step == null) {
2!
560
      return this.context.success();
4✔
561
    } else {
562
      return step.asSuccess();
×
563
    }
564
  }
565

566

567
  /**
568
   * @param step the {@link Step} to get {@link Step#asError() error logger} from. May be {@code null}.
569
   * @return the {@link IdeSubLogger} from {@link Step#asError()} or {@link IdeContext#error()} as fallback.
570
   */
571
  protected IdeSubLogger asError(Step step) {
572

573
    if (step == null) {
×
574
      return this.context.error();
×
575
    } else {
576
      return step.asError();
×
577
    }
578
  }
579
}
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