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

devonfw / IDEasy / 20817627478

08 Jan 2026 12:58PM UTC coverage: 69.884% (-0.02%) from 69.902%
20817627478

Pull #1672

github

web-flow
Merge 3ff82e66f into a329c529a
Pull Request #1672: #1667: fix ide env triggering unintended tool installations

3983 of 6278 branches covered (63.44%)

Branch coverage included in aggregate %.

10195 of 14010 relevant lines covered (72.77%)

3.14 hits per line

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

81.61
cli/src/main/java/com/devonfw/tools/ide/tool/PackageManagerBasedLocalToolCommandlet.java
1
package com.devonfw.tools.ide.tool;
2

3
import java.nio.file.Files;
4
import java.nio.file.Path;
5
import java.util.List;
6
import java.util.Set;
7

8
import com.devonfw.tools.ide.cache.CachedValue;
9
import com.devonfw.tools.ide.common.Tag;
10
import com.devonfw.tools.ide.context.IdeContext;
11
import com.devonfw.tools.ide.process.ProcessContext;
12
import com.devonfw.tools.ide.process.ProcessErrorHandling;
13
import com.devonfw.tools.ide.process.ProcessMode;
14
import com.devonfw.tools.ide.process.ProcessResult;
15
import com.devonfw.tools.ide.version.VersionIdentifier;
16

17
/**
18
 * {@link LocalToolCommandlet} for tools that have their own {@link #getToolRepository() repository} and do not follow standard installation mechanism.
19
 *
20
 * @param <P> type of the {@link ToolCommandlet} acting as {@link #getPackageManagerClass() package manager}.
21
 */
22
public abstract class PackageManagerBasedLocalToolCommandlet<P extends ToolCommandlet> extends LocalToolCommandlet {
23

24
  private final CachedValue<VersionIdentifier> installedVersion;
25

26
  /**
27
   * The constructor.
28
   *
29
   * @param context the {@link IdeContext}.
30
   * @param tool the {@link #getName() tool name}.
31
   * @param tags the {@link #getTags() tags} classifying the tool. Should be created via {@link Set#of(Object) Set.of} method.
32
   */
33
  public PackageManagerBasedLocalToolCommandlet(IdeContext context, String tool, Set<Tag> tags) {
34

35
    super(context, tool, tags);
5✔
36
    this.installedVersion = new CachedValue<>(this::determineInstalledVersion);
7✔
37
  }
1✔
38

39
  @Override
40
  protected boolean isIgnoreSoftwareRepo() {
41

42
    // python/pip and node/npm/yarn are messy - see https://github.com/devonfw/IDEasy/issues/352
43
    return true;
2✔
44
  }
45

46
  @Override
47
  public boolean isInstalled() {
48

49
    // Check if parent tool is installed first - if not, this tool cannot be installed
50
    LocalToolCommandlet parentTool = getParentTool();
3✔
51
    if (!parentTool.isInstalled()) {
3!
52
      return false;
2✔
53
    }
54

55
    // Check if the binary exists in the tool bin path
56
    Path toolBinPath = getToolBinPath();
×
57
    if (toolBinPath == null || !Files.isDirectory(toolBinPath)) {
×
58
      return false;
×
59
    }
60

61
    // Use SystemPath to properly resolve binary with platform-specific extensions (.exe, .cmd, .bat on Windows)
62
    Path binaryPath = toolBinPath.resolve(getBinaryName());
×
63
    Path resolvedBinary = this.context.getPath().findBinary(binaryPath);
×
64
    
65
    // findBinary returns the original path if not found, so check if it actually exists
66
    return Files.exists(resolvedBinary);
×
67
  }
68

69
  protected abstract Class<P> getPackageManagerClass();
70

71
  /**
72
   * @return the package name of this tool in the underlying repository (e.g. MVN repo, NPM registry, PyPI). Typically, this is the same as the
73
   *     {@link #getName() tool name} but may be overridden in some special cases.
74
   */
75
  public String getPackageName() {
76

77
    return this.tool;
3✔
78
  }
79

80
  /**
81
   * @param request the {@link PackageManagerRequest}.
82
   * @return the {@link ProcessResult}.
83
   */
84
  public ProcessResult runPackageManager(PackageManagerRequest request) {
85
    return runPackageManager(request, false);
5✔
86
  }
87

88
  /**
89
   * @param request the {@link PackageManagerRequest}.
90
   * @param skipInstallation {@code true} if the caller can guarantee that this package manager tool is already installed, {@code false} otherwise (run
91
   *     install method again to ensure the tool is installed).
92
   * @return the {@link ProcessResult}.
93
   */
94
  public ProcessResult runPackageManager(PackageManagerRequest request, boolean skipInstallation) {
95

96
    completeRequest(request);
3✔
97
    ProcessContext pc = request.getProcessContext();
3✔
98
    ToolCommandlet pm = request.getPackageManager();
3✔
99
    if (skipInstallation) { // See Node.postInstallOnNewInstallation
2✔
100
      return pm.runTool(pc, request.getProcessMode(), request.getArgs());
8✔
101
    } else {
102
      ToolInstallRequest installRequest = new ToolInstallRequest(true);
5✔
103
      installRequest.setProcessContext(pc);
3✔
104
      return pm.runTool(installRequest, request.getProcessMode(), request.getArgs());
8✔
105
    }
106
  }
107

108
  protected void completeRequest(PackageManagerRequest request) {
109

110
    if (request.getProcessContext() == null) {
3✔
111
      request.setProcessContext(this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_CLI));
8✔
112
    }
113
    if (request.getPackageManager() == null) {
3✔
114
      request.setPackageManager(this.context.getCommandletManager().getCommandlet(getPackageManagerClass()));
10✔
115
    }
116
    if (request.getProcessMode() == null) {
3!
117
      request.setProcessMode(ProcessMode.DEFAULT);
4✔
118
    }
119
    if (request.getArgs().isEmpty()) {
4✔
120
      completeRequestArgs(request);
3✔
121
    }
122
  }
1✔
123

124
  /**
125
   * @param request the {@link PackageManagerRequest} with currently {@link List#isEmpty() empty} {@link PackageManagerRequest#getArgs() args}.
126
   */
127
  protected void completeRequestArgs(PackageManagerRequest request) {
128

129
    String toolWithVersion = request.getTool();
3✔
130
    VersionIdentifier version = request.getVersion();
3✔
131
    if (version != null) {
2✔
132
      toolWithVersion = appendVersion(toolWithVersion, version);
5✔
133
    }
134
    request.addArg(request.getType());
5✔
135
    String option = completeRequestOption(request);
4✔
136
    if (option != null) {
2✔
137
      request.addArg(option);
4✔
138
    }
139
    request.addArg(toolWithVersion);
4✔
140
  }
1✔
141

142
  /**
143
   * @param request the {@link PackageManagerRequest}.
144
   * @return the option to {@link PackageManagerRequest#addArg(String) add as argument} to the package manager sub-command (e.g. "-gf") or {@code null} for no
145
   *     option.
146
   */
147
  protected String completeRequestOption(PackageManagerRequest request) {
148
    return null;
2✔
149
  }
150

151
  /**
152
   * @param tool the {@link PackageManagerRequest#getTool() tool} to manage (e.g. install).
153
   * @param version the {@link PackageManagerRequest#getVersion() version} to append.
154
   * @return the combination of {@code tool} and {@code version} in the syntax of the package manager.
155
   */
156
  protected String appendVersion(String tool, VersionIdentifier version) {
157
    return tool + '@' + version;
5✔
158
  }
159

160
  @Override
161
  protected boolean isIgnoreMissingSoftwareVersionFile() {
162

163
    return true;
×
164
  }
165

166
  private VersionIdentifier determineInstalledVersion() {
167

168
    try {
169
      return computeInstalledVersion();
3✔
170
    } catch (Exception e) {
×
171
      this.context.debug().log(e, "Failed to compute installed version of {}", this.tool);
×
172
      return null;
×
173
    }
174
  }
175

176
  /**
177
   * @return the computed value of the {@link #getInstalledVersion() installed version}.
178
   * @implNote Implementations of this method should NOT trigger any tool installation or download. If you need to call
179
   *     {@link #runPackageManager(PackageManagerRequest)}, make sure to use
180
   *     {@link #runPackageManager(PackageManagerRequest, boolean)} with {@code skipInstallation=true} to avoid
181
   *     inadvertently triggering installations when only checking the version.
182
   */
183
  protected abstract VersionIdentifier computeInstalledVersion();
184

185
  @Override
186
  public VersionIdentifier getInstalledVersion() {
187

188
    return this.installedVersion.get();
5✔
189
  }
190

191
  @Override
192
  protected final void performToolInstallation(ToolInstallRequest request, Path installationPath) {
193

194
    PackageManagerRequest packageManagerRequest = new PackageManagerRequest(PackageManagerRequest.TYPE_INSTALL, getPackageName())
7✔
195
        .setProcessContext(request.getProcessContext()).setVersion(request.getRequested().getResolvedVersion());
7✔
196
    runPackageManager(packageManagerRequest, true).failOnError();
5✔
197
    this.installedVersion.invalidate();
3✔
198
  }
1✔
199

200
  /**
201
   * @return {@code true} if the tool can be uninstalled, {@code false} if not.
202
   */
203
  protected boolean canBeUninstalled() {
204
    return true;
2✔
205
  }
206

207
  @Override
208
  protected final void performUninstall(Path toolPath) {
209
    if (canBeUninstalled()) {
3✔
210
      PackageManagerRequest request = new PackageManagerRequest(PackageManagerRequest.TYPE_UNINSTALL, getPackageName());
7✔
211
      runPackageManager(request).failOnError();
4✔
212
      this.installedVersion.invalidate();
3✔
213
    } else {
1✔
214
      this.context.info("IDEasy does not support uninstalling the tool {} since this will break your installation.\n"
14✔
215
          + "If you really want to uninstall it, please uninstall its parent tool via:\n"
216
          + "ide uninstall {}", this.tool, getParentTool().getName());
3✔
217
    }
218
  }
1✔
219

220
  protected abstract LocalToolCommandlet getParentTool();
221

222
  @Override
223
  public Path getToolPath() {
224

225
    return getParentTool().getToolPath();
4✔
226
  }
227

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