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

devonfw / IDEasy / 7278937169

20 Dec 2023 05:43PM UTC coverage: 47.802% (-0.07%) from 47.873%
7278937169

Pull #151

github

web-flow
Merge 09b6148ea into 1d60d9c17
Pull Request #151: #20: Implement ToolCommandlet for GCViewer

1031 of 2383 branches covered (0.0%)

Branch coverage included in aggregate %.

2721 of 5466 relevant lines covered (49.78%)

2.05 hits per line

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

24.88
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.nio.file.Paths;
7
import java.util.List;
8
import java.util.Set;
9
import java.util.stream.Stream;
10

11
import com.devonfw.tools.ide.cli.CliException;
12
import com.devonfw.tools.ide.commandlet.Commandlet;
13
import com.devonfw.tools.ide.common.Tags;
14
import com.devonfw.tools.ide.context.IdeContext;
15
import com.devonfw.tools.ide.environment.EnvironmentVariables;
16
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
17
import com.devonfw.tools.ide.io.FileAccess;
18
import com.devonfw.tools.ide.io.TarCompression;
19
import com.devonfw.tools.ide.os.MacOsHelper;
20
import com.devonfw.tools.ide.process.ProcessContext;
21
import com.devonfw.tools.ide.process.ProcessErrorHandling;
22
import com.devonfw.tools.ide.property.StringListProperty;
23
import com.devonfw.tools.ide.util.FilenameUtil;
24
import com.devonfw.tools.ide.version.VersionIdentifier;
25

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

31
  /** @see #getName() */
32
  protected final String tool;
33

34
  private final Set<String> tags;
35

36
  /** The commandline arguments to pass to the tool. */
37
  public final StringListProperty arguments;
38

39
  private MacOsHelper macOsHelper;
40

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

51
    super(context);
3✔
52
    this.tool = tool;
3✔
53
    this.tags = tags;
3✔
54
    addKeyword(tool);
3✔
55
    this.arguments = add(new StringListProperty("", false, "args"));
11✔
56
  }
1✔
57

58
  /**
59
   * @return the name of the tool (e.g. "java", "mvn", "npm", "node").
60
   */
61
  @Override
62
  public String getName() {
63

64
    return this.tool;
3✔
65
  }
66

67
  /**
68
   * @return the name of the binary executable for this tool.
69
   */
70
  protected String getBinaryName() {
71

72
    return this.tool;
×
73
  }
74

75
  @Override
76
  public final Set<String> getTags() {
77

78
    return this.tags;
×
79
  }
80

81
  @Override
82
  public void run() {
83

84
    runTool(null, this.arguments.asArray());
×
85
  }
×
86

87
  /**
88
   * Ensures the tool is installed and then runs this tool with the given arguments.
89
   *
90
   * @param toolVersion the explicit version (pattern) to run. Typically {@code null} to ensure the configured version
91
   *        is installed and use that one. Otherwise the specified version will be installed in the software repository
92
   *        without touching and IDE installation and used to run.
93
   * @param args the commandline arguments to run the tool.
94
   */
95
  public void runTool(VersionIdentifier toolVersion, String... args) {
96

97
    Path binaryPath;
98
    Path toolPath = Paths.get(getBinaryName());
×
99
    if (toolVersion == null) {
×
100
      install(true);
×
101
      binaryPath = toolPath;
×
102
    } else {
103
      throw new UnsupportedOperationException("Not yet implemented!");
×
104
    }
105
    ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.WARNING).executable(binaryPath)
×
106
        .addArgs(args);
×
107
    pc.run();
×
108
  }
×
109

110
  /**
111
   * @return the {@link EnvironmentVariables#getToolEdition(String) tool edition}.
112
   */
113
  protected String getEdition() {
114

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

118
  /**
119
   * @return the {@link #getName() tool} with its {@link #getEdition() edition}. The edition will be omitted if same as
120
   *         tool.
121
   * @see #getToolWithEdition(String, String)
122
   */
123
  protected final String getToolWithEdition() {
124

125
    return getToolWithEdition(getName(), getEdition());
×
126
  }
127

128
  /**
129
   * @param tool the tool name.
130
   * @param edition the edition.
131
   * @return the {@link #getName() tool} with its {@link #getEdition() edition}. The edition will be omitted if same as
132
   *         tool.
133
   */
134
  protected final static String getToolWithEdition(String tool, String edition) {
135

136
    if (tool.equals(edition)) {
×
137
      return tool;
×
138
    }
139
    return tool + "/" + edition;
×
140
  }
141

142
  /**
143
   * @return the {@link EnvironmentVariables#getToolVersion(String) tool version}.
144
   */
145
  public VersionIdentifier getConfiguredVersion() {
146

147
    return this.context.getVariables().getToolVersion(getName());
×
148
  }
149

150
  /**
151
   * Method to be called for {@link #install(boolean)} from dependent {@link Commandlet}s.
152
   *
153
   * @return {@code true} if the tool was newly installed, {@code false} if the tool was already installed before and
154
   *         nothing has changed.
155
   */
156
  public boolean install() {
157

158
    return install(true);
×
159
  }
160

161
  /**
162
   * Performs the installation of the {@link #getName() tool} managed by this {@link Commandlet}.
163
   *
164
   * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
165
   * @return {@code true} if the tool was newly installed, {@code false} if the tool was already installed before and
166
   *         nothing has changed.
167
   */
168
  public boolean install(boolean silent) {
169

170
    return doInstall(silent);
×
171
  }
172

173
  /**
174
   * Installs or updates the managed {@link #getName() tool}.
175
   *
176
   * @param silent - {@code true} if called recursively to suppress verbose logging, {@code false} otherwise.
177
   * @return {@code true} if the tool was newly installed, {@code false} if the tool was already installed before and
178
   *         nothing has changed.
179
   */
180
  protected abstract boolean doInstall(boolean silent);
181

182
  /**
183
   * This method is called after the tool has been newly installed or updated to a new version.
184
   */
185
  protected void postInstall() {
186

187
    // nothing to do by default
188
  }
×
189

190
  /**
191
   * @param path the {@link Path} to start the recursive search from.
192
   * @return the deepest subdir {@code s} of the passed path such that all directories between {@code s} and the passed
193
   *         path (including {@code s}) are the sole item in their respective directory and {@code s} is not named
194
   *         "bin".
195
   */
196
  private Path getProperInstallationSubDirOf(Path path) {
197

198
    try (Stream<Path> stream = Files.list(path)) {
×
199
      Path[] subFiles = stream.toArray(Path[]::new);
×
200
      if (subFiles.length == 0) {
×
201
        throw new CliException("The downloaded package for the tool " + this.tool
×
202
            + " seems to be empty as you can check in the extracted folder " + path);
203
      } else if (subFiles.length == 1) {
×
204
        String filename = subFiles[0].getFileName().toString();
×
205
        if (!filename.equals(IdeContext.FOLDER_BIN) && !filename.equals(IdeContext.FOLDER_CONTENTS)
×
206
            && !filename.endsWith(".app") && Files.isDirectory(subFiles[0])) {
×
207
          return getProperInstallationSubDirOf(subFiles[0]);
×
208
        }
209
      }
210
      return path;
×
211
    } catch (IOException e) {
×
212
      throw new IllegalStateException("Failed to get sub-files of " + path);
×
213
    }
214
  }
215

216
  /**
217
   * @param file the {@link Path} to the file to extract.
218
   * @param targetDir the {@link Path} to the directory where to extract (or copy) the file.
219
   */
220
  protected void extract(Path file, Path targetDir) {
221

222
    FileAccess fileAccess = this.context.getFileAccess();
×
223
    if (isExtract()) {
×
224
      Path tmpDir = this.context.getFileAccess().createTempDir("extract-" + file.getFileName());
×
225
      this.context.trace("Trying to extract the downloaded file {} to {} and move it to {}.", file, tmpDir, targetDir);
×
226
      String extension = FilenameUtil.getExtension(file.getFileName().toString());
×
227
      this.context.trace("Determined file extension {}", extension);
×
228
      TarCompression tarCompression = TarCompression.of(extension);
×
229
      if (tarCompression != null) {
×
230
        fileAccess.untar(file, tmpDir, tarCompression);
×
231
      } else if ("zip".equals(extension) || "jar".equals(extension)) {
×
232
        fileAccess.unzip(file, tmpDir);
×
233
      } else if ("dmg".equals(extension)) {
×
234
        assert this.context.getSystemInfo().isMac();
×
235
        Path mountPath = this.context.getIdeHome().resolve(IdeContext.FOLDER_UPDATES).resolve(IdeContext.FOLDER_VOLUME);
×
236
        fileAccess.mkdirs(mountPath);
×
237
        ProcessContext pc = this.context.newProcess();
×
238
        pc.executable("hdiutil");
×
239
        pc.addArgs("attach", "-quiet", "-nobrowse", "-mountpoint", mountPath, file);
×
240
        pc.run();
×
241
        Path appPath = fileAccess.findFirst(mountPath, p -> p.getFileName().toString().endsWith(".app"), false);
×
242
        if (appPath == null) {
×
243
          throw new IllegalStateException("Failed to unpack DMG as no MacOS *.app was found in file " + file);
×
244
        }
245
        fileAccess.copy(appPath, tmpDir);
×
246
        pc.addArgs("detach", "-force", mountPath);
×
247
        pc.run();
×
248
        // if [ -e "${target_dir}/Applications" ]
249
        // then
250
        // rm "${target_dir}/Applications"
251
        // fi
252
      } else if ("msi".equals(extension)) {
×
253
        this.context.newProcess().executable("msiexec").addArgs("/a", file, "/qn", "TARGETDIR=" + tmpDir).run();
×
254
        // msiexec also creates a copy of the MSI
255
        Path msiCopy = tmpDir.resolve(file.getFileName());
×
256
        fileAccess.delete(msiCopy);
×
257
      } else if ("pkg".equals(extension)) {
×
258

259
        Path tmpDirPkg = fileAccess.createTempDir("ide-pkg-");
×
260
        ProcessContext pc = this.context.newProcess();
×
261
        // we might also be able to use cpio from commons-compression instead of external xar...
262
        pc.executable("xar").addArgs("-C", tmpDirPkg, "-xf", file).run();
×
263
        Path contentPath = fileAccess.findFirst(tmpDirPkg, p -> p.getFileName().toString().equals("Payload"), true);
×
264
        fileAccess.untar(contentPath, tmpDir, TarCompression.GZ);
×
265
        fileAccess.delete(tmpDirPkg);
×
266
      } else {
×
267
        throw new IllegalStateException("Unknown archive format " + extension + ". Can not extract " + file);
×
268
      }
269
      fileAccess.move(getProperInstallationSubDirOf(tmpDir), targetDir);
×
270
      fileAccess.delete(tmpDir);
×
271
    } else {
×
272
      this.context.trace("Extraction is disabled for '{}' hence just moving the downloaded file {}.", getName(), file);
×
273

274
      try {
275
        Files.createDirectories(targetDir);
×
276
      } catch (IOException e) {
×
277
        throw new IllegalStateException("Failed to create folder " + targetDir);
×
278
      }
×
279
      fileAccess.move(file, targetDir.resolve(file.getFileName()));
×
280
    }
281
  }
×
282

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

288
    return true;
×
289
  }
290

291
  /**
292
   * @return the {@link MacOsHelper} instance.
293
   */
294
  protected MacOsHelper getMacOsHelper() {
295

296
    if (this.macOsHelper == null) {
×
297
      this.macOsHelper = new MacOsHelper(this.context);
×
298
    }
299
    return this.macOsHelper;
×
300
  }
301

302
  /**
303
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
304
   */
305
  public VersionIdentifier getInstalledVersion() {
306

307
    return getInstalledVersion(this.context.getSoftwarePath().resolve(getName()));
9✔
308
  }
309

310
  /**
311
   * @param toolPath the installation {@link Path} where to find the version file.
312
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
313
   */
314
  protected VersionIdentifier getInstalledVersion(Path toolPath) {
315

316
    if (!Files.isDirectory(toolPath)) {
5!
317
      this.context.debug("Tool {} not installed in {}", getName(), toolPath);
×
318
      return null;
×
319
    }
320
    Path toolVersionFile = toolPath.resolve(IdeContext.FILE_SOFTWARE_VERSION);
4✔
321
    if (!Files.exists(toolVersionFile)) {
5!
322
      Path legacyToolVersionFile = toolPath.resolve(IdeContext.FILE_LEGACY_SOFTWARE_VERSION);
4✔
323
      if (Files.exists(legacyToolVersionFile)) {
5✔
324
        toolVersionFile = legacyToolVersionFile;
3✔
325
      } else {
326
        this.context.warning("Tool {} is missing version file in {}", getName(), toolVersionFile);
15✔
327
        return null;
2✔
328
      }
329
    }
330
    try {
331
      String version = Files.readString(toolVersionFile).trim();
4✔
332
      return VersionIdentifier.of(version);
3✔
333
    } catch (IOException e) {
×
334
      throw new IllegalStateException("Failed to read file " + toolVersionFile, e);
×
335
    }
336
  }
337

338
  /**
339
   * List the available versions of this tool.
340
   */
341
  public void listVersions() {
342

343
    List<VersionIdentifier> versions = this.context.getUrls().getSortedVersions(getName(), getEdition());
9✔
344
    for (VersionIdentifier vi : versions) {
10✔
345
      this.context.info(vi.toString());
5✔
346
    }
1✔
347
  }
1✔
348

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

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

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

374
    EnvironmentVariables variables = this.context.getVariables();
4✔
375
    EnvironmentVariables settingsVariables = variables.getByType(EnvironmentVariablesType.SETTINGS);
4✔
376
    String edition = getEdition();
3✔
377
    String name = EnvironmentVariables.getToolVersionVariable(this.tool);
4✔
378
    VersionIdentifier resolvedVersion = this.context.getUrls().getVersion(this.tool, edition, version);
9✔
379
    if (version.isPattern()) {
3!
380
      this.context.debug("Resolved version {} to {} for tool {}/{}", version, resolvedVersion, this.tool, edition);
×
381
    }
382
    settingsVariables.set(name, resolvedVersion.toString(), false);
7✔
383
    settingsVariables.save();
2✔
384
    this.context.info("{}={} has been set in {}", name, version, settingsVariables.getSource());
19✔
385
    EnvironmentVariables declaringVariables = variables.findVariable(name);
4✔
386
    if ((declaringVariables != null) && (declaringVariables != settingsVariables)) {
5!
387
      this.context.warning(
×
388
          "The variable {} is overridden in {}. Please remove the overridden declaration in order to make the change affect.",
389
          name, declaringVariables.getSource());
×
390
    }
391
    if (hint) {
2!
392
      this.context.info("To install that version call the following command:");
4✔
393
      this.context.info("ide install {}", this.tool);
11✔
394
    }
395
  }
1✔
396

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