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

devonfw / IDEasy / 18949830015

30 Oct 2025 05:38PM UTC coverage: 68.876% (-0.004%) from 68.88%
18949830015

push

github

web-flow
#1553: speed up environment commandlet (#1554)

Co-authored-by: jan-vcapgemini <59438728+jan-vcapgemini@users.noreply.github.com>

3486 of 5545 branches covered (62.87%)

Branch coverage included in aggregate %.

9121 of 12759 relevant lines covered (71.49%)

3.14 hits per line

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

66.2
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.Set;
8

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

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

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

37
  private final Set<Tag> tags;
38

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

42
  private Path executionDirectory;
43

44
  private MacOsHelper macOsHelper;
45

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

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

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

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

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

77
    return this.tool;
3✔
78
  }
79

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

85
    return this.tool;
3✔
86
  }
87

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

91
    return this.tags;
3✔
92
  }
93

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

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

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

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

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

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

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

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

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

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

148
  @Override
149
  public void run() {
150

151
    runTool(this.arguments.asArray());
6✔
152
  }
1✔
153

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

161
    return runTool(ProcessMode.DEFAULT, null, args);
6✔
162
  }
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
   * @return the {@link ProcessResult result}.
172
   */
173
  public final ProcessResult runTool(ProcessMode processMode, GenericVersionRange toolVersion, String... args) {
174

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

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

190
    if (toolVersion != null) {
2!
191
      throw new UnsupportedOperationException("Not implemented yet");
×
192
    }
193
    ProcessContext pc = this.context.newProcess().errorHandling(errorHandling);
6✔
194
    install(true, pc, null);
6✔
195
    return runTool(processMode, errorHandling, pc, args);
7✔
196
  }
197

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

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

215
  /**
216
   * @param pc the {@link ProcessContext}.
217
   * @param processMode the {@link ProcessMode}.
218
   * @param errorHandling the {@link ProcessErrorHandling}.
219
   */
220
  protected void configureToolBinary(ProcessContext pc, ProcessMode processMode, ProcessErrorHandling errorHandling) {
221

222
    pc.executable(Path.of(getBinaryName()));
8✔
223
  }
1✔
224

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

233
    pc.addArgs(args);
4✔
234
  }
1✔
235

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

245
    return this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_ERR).executable(binaryPath).addArgs(args);
×
246
  }
247

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

255
    return install(true);
4✔
256
  }
257

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

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

280
  /**
281
   * @return {@code true} to extract (unpack) the downloaded binary file, {@code false} otherwise.
282
   */
283
  protected boolean isExtract() {
284

285
    return true;
2✔
286
  }
287

288
  /**
289
   * @return the {@link MacOsHelper} instance.
290
   */
291
  protected MacOsHelper getMacOsHelper() {
292

293
    if (this.macOsHelper == null) {
3✔
294
      this.macOsHelper = new MacOsHelper(this.context);
7✔
295
    }
296
    return this.macOsHelper;
3✔
297
  }
298

299
  /**
300
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
301
   */
302
  public abstract VersionIdentifier getInstalledVersion();
303

304
  /**
305
   * @return {@code true} if this tool is installed, {@code false} otherwise.
306
   */
307
  public boolean isInstalled() {
308

309
    return getInstalledVersion() != null;
7✔
310
  }
311

312
  /**
313
   * @return the installed edition of this tool or {@code null} if not installed.
314
   */
315
  public abstract String getInstalledEdition();
316

317
  /**
318
   * Uninstalls the {@link #getName() tool}.
319
   */
320
  public abstract void uninstall();
321

322
  /**
323
   * @return the {@link ToolRepository}.
324
   */
325
  public ToolRepository getToolRepository() {
326

327
    return this.context.getDefaultToolRepository();
4✔
328
  }
329

330
  /**
331
   * List the available editions of this tool.
332
   */
333
  public void listEditions() {
334

335
    List<String> editions = getToolRepository().getSortedEditions(getName());
6✔
336
    for (String edition : editions) {
10✔
337
      this.context.info(edition);
4✔
338
    }
1✔
339
  }
1✔
340

341
  /**
342
   * List the available versions of this tool.
343
   */
344
  public void listVersions() {
345

346
    List<VersionIdentifier> versions = getToolRepository().getSortedVersions(getName(), getConfiguredEdition(), this);
9✔
347
    for (VersionIdentifier vi : versions) {
10✔
348
      this.context.info(vi.toString());
5✔
349
    }
1✔
350
  }
1✔
351

352
  /**
353
   * Sets the tool version in the environment variable configuration file.
354
   *
355
   * @param version the version (pattern) to set.
356
   */
357
  public void setVersion(String version) {
358

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

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

377
    setVersion(version, hint, null);
5✔
378
  }
1✔
379

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

389
    String edition = getConfiguredEdition();
3✔
390
    ToolRepository toolRepository = getToolRepository();
3✔
391

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

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

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

421
    setEdition(edition, true);
4✔
422
  }
1✔
423

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

432
    setEdition(edition, hint, null);
5✔
433
  }
1✔
434

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

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

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

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

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

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

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

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

494
    return null;
×
495
  }
496

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

506
    createStartScript(targetDir, binary, false);
×
507
  }
×
508

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

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

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

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

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

564

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

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

© 2025 Coveralls, Inc