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

devonfw / IDEasy / 7725548339

31 Jan 2024 11:47AM UTC coverage: 55.799% (-0.2%) from 56.041%
7725548339

Pull #122

github

web-flow
Merge c35889099 into 8746243dc
Pull Request #122: #13: implement tool commandlet for aws cli

1421 of 2787 branches covered (0.0%)

Branch coverage included in aggregate %.

3650 of 6301 relevant lines covered (57.93%)

2.49 hits per line

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

45.41
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.Tag;
14
import com.devonfw.tools.ide.common.Tags;
15
import com.devonfw.tools.ide.context.IdeContext;
16
import com.devonfw.tools.ide.environment.EnvironmentVariables;
17
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
18
import com.devonfw.tools.ide.io.FileAccess;
19
import com.devonfw.tools.ide.io.TarCompression;
20
import com.devonfw.tools.ide.os.MacOsHelper;
21
import com.devonfw.tools.ide.process.ProcessContext;
22
import com.devonfw.tools.ide.process.ProcessErrorHandling;
23
import com.devonfw.tools.ide.property.StringListProperty;
24
import com.devonfw.tools.ide.util.FilenameUtil;
25
import com.devonfw.tools.ide.version.VersionIdentifier;
26

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

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

35
  private final Set<Tag> tags;
36

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

40
  private MacOsHelper macOsHelper;
41

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

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

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

65
    return this.tool;
3✔
66
  }
67

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

73
    return this.tool;
×
74
  }
75

76
  @Override
77
  public final Set<Tag> getTags() {
78

79
    return this.tags;
×
80
  }
81

82
  @Override
83
  public void run() {
84

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

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

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

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

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

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

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

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

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

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

148
    return this.context.getVariables().getToolVersion(getName());
7✔
149
  }
150

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

159
    return install(true);
×
160
  }
161

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

171
    return doInstall(silent);
4✔
172
  }
173

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

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

188
    // nothing to do by default
189
  }
1✔
190

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

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

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

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

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

275
      if (Files.isDirectory(file)) {
×
276
        fileAccess.move(file, targetDir);
×
277
      } else {
278
        try {
279
          Files.createDirectories(targetDir);
×
280
        } catch (IOException e) {
×
281
          throw new IllegalStateException("Failed to create folder " + targetDir);
×
282
        }
×
283
        fileAccess.move(file, targetDir.resolve(file.getFileName()));
×
284
      }
285
    }
286
  }
1✔
287

288
  protected void moveAndProcessExtraction(Path from, Path to) {
289

290
    this.context.getFileAccess().move(from, to);
6✔
291
  }
1✔
292

293
  /**
294
   * @return {@code true} to extract (unpack) the downloaded binary file, {@code false} otherwise.
295
   */
296
  protected boolean isExtract() {
297

298
    return true;
2✔
299
  }
300

301
  /**
302
   * @return the {@link MacOsHelper} instance.
303
   */
304
  protected MacOsHelper getMacOsHelper() {
305

306
    if (this.macOsHelper == null) {
3!
307
      this.macOsHelper = new MacOsHelper(this.context);
7✔
308
    }
309
    return this.macOsHelper;
3✔
310
  }
311

312
  /**
313
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
314
   */
315
  public VersionIdentifier getInstalledVersion() {
316

317
    return getInstalledVersion(this.context.getSoftwarePath().resolve(getName()));
9✔
318
  }
319

320
  /**
321
   * @param toolPath the installation {@link Path} where to find the version file.
322
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
323
   */
324
  protected VersionIdentifier getInstalledVersion(Path toolPath) {
325

326
    if (!Files.isDirectory(toolPath)) {
5!
327
      this.context.debug("Tool {} not installed in {}", getName(), toolPath);
×
328
      return null;
×
329
    }
330
    Path toolVersionFile = toolPath.resolve(IdeContext.FILE_SOFTWARE_VERSION);
4✔
331
    if (!Files.exists(toolVersionFile)) {
5!
332
      Path legacyToolVersionFile = toolPath.resolve(IdeContext.FILE_LEGACY_SOFTWARE_VERSION);
4✔
333
      if (Files.exists(legacyToolVersionFile)) {
5✔
334
        toolVersionFile = legacyToolVersionFile;
3✔
335
      } else {
336
        this.context.warning("Tool {} is missing version file in {}", getName(), toolVersionFile);
15✔
337
        return null;
2✔
338
      }
339
    }
340
    try {
341
      String version = Files.readString(toolVersionFile).trim();
4✔
342
      return VersionIdentifier.of(version);
3✔
343
    } catch (IOException e) {
×
344
      throw new IllegalStateException("Failed to read file " + toolVersionFile, e);
×
345
    }
346
  }
347

348
  /**
349
   * List the available versions of this tool.
350
   */
351
  public void listVersions() {
352

353
    List<VersionIdentifier> versions = this.context.getUrls().getSortedVersions(getName(), getEdition());
9✔
354
    for (VersionIdentifier vi : versions) {
10✔
355
      this.context.info(vi.toString());
5✔
356
    }
1✔
357
  }
1✔
358

359
  /**
360
   * Sets the tool version in the environment variable configuration file.
361
   *
362
   * @param version the version (pattern) to set.
363
   */
364
  public void setVersion(String version) {
365

366
    if ((version == null) || version.isBlank()) {
×
367
      throw new IllegalStateException("Version has to be specified!");
×
368
    }
369
    VersionIdentifier configuredVersion = VersionIdentifier.of(version);
×
370
    if (!configuredVersion.isPattern() && !configuredVersion.isValid()) {
×
371
      this.context.warning("Version {} seems to be invalid", version);
×
372
    }
373
    setVersion(configuredVersion, true);
×
374
  }
×
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
   */
382
  public void setVersion(VersionIdentifier version, boolean hint) {
383

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

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