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

devonfw / IDEasy / 13630835369

03 Mar 2025 12:25PM UTC coverage: 68.251%. Remained the same
13630835369

Pull #1101

github

web-flow
Merge b8d15e9fb into 991139853
Pull Request #1101: #910 Can not update intellij on linux

3032 of 4891 branches covered (61.99%)

Branch coverage included in aggregate %.

7867 of 11078 relevant lines covered (71.01%)

3.09 hits per line

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

86.75
cli/src/main/java/com/devonfw/tools/ide/tool/LocalToolCommandlet.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.LinkOption;
6
import java.nio.file.Path;
7
import java.nio.file.StandardOpenOption;
8
import java.util.Collection;
9
import java.util.Locale;
10
import java.util.Set;
11

12
import com.devonfw.tools.ide.common.Tag;
13
import com.devonfw.tools.ide.context.IdeContext;
14
import com.devonfw.tools.ide.io.FileAccess;
15
import com.devonfw.tools.ide.io.FileCopyMode;
16
import com.devonfw.tools.ide.process.EnvironmentContext;
17
import com.devonfw.tools.ide.step.Step;
18
import com.devonfw.tools.ide.tool.repository.ToolRepository;
19
import com.devonfw.tools.ide.url.model.file.json.ToolDependency;
20
import com.devonfw.tools.ide.version.GenericVersionRange;
21
import com.devonfw.tools.ide.version.VersionIdentifier;
22
import com.devonfw.tools.ide.version.VersionRange;
23

24
/**
25
 * {@link ToolCommandlet} that is installed locally into the IDEasy.
26
 */
27
public abstract class LocalToolCommandlet extends ToolCommandlet {
1✔
28

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

38
    super(context, tool, tags);
5✔
39
  }
1✔
40

41
  /**
42
   * @return the {@link Path} where the tool is located (installed).
43
   */
44
  public Path getToolPath() {
45

46
    return this.context.getSoftwarePath().resolve(getName());
7✔
47
  }
48

49
  /**
50
   * @return the {@link Path} where the executables of the tool can be found. Typically a "bin" folder inside {@link #getToolPath() tool path}.
51
   */
52
  public Path getToolBinPath() {
53

54
    Path toolPath = getToolPath();
3✔
55
    Path binPath = this.context.getFileAccess().findFirst(toolPath, path -> path.getFileName().toString().equals("bin"), false);
14✔
56
    if ((binPath != null) && Files.isDirectory(binPath)) {
7!
57
      return binPath;
2✔
58
    }
59
    return toolPath;
2✔
60
  }
61

62
  /**
63
   * @deprecated will be removed once all "dependencies.json" are created in ide-urls.
64
   */
65
  @Deprecated
66
  protected void installDependencies() {
67

68
  }
1✔
69

70
  @Override
71
  public boolean install(boolean silent, EnvironmentContext environmentContext) {
72

73
    installDependencies();
2✔
74
    VersionIdentifier configuredVersion = getConfiguredVersion();
3✔
75
    // get installed version before installInRepo actually may install the software
76
    VersionIdentifier installedVersion = getInstalledVersion();
3✔
77
    Step step = this.context.newStep(silent, "Install " + this.tool, configuredVersion);
14✔
78
    try {
79
      // install configured version of our tool in the software repository if not already installed
80
      ToolInstallation installation = installTool(configuredVersion, environmentContext);
5✔
81

82
      // check if we already have this version installed (linked) locally in IDE_HOME/software
83
      VersionIdentifier resolvedVersion = installation.resolvedVersion();
3✔
84
      if ((resolvedVersion.equals(installedVersion) && !installation.newInstallation())
9!
85
          || (configuredVersion.matches(installedVersion) && context.isSkipUpdatesMode())) {
2!
86
        return toolAlreadyInstalled(silent, installedVersion, step);
8✔
87
      }
88
      if (!isIgnoreSoftwareRepo()) {
3✔
89
        // we need to link the version or update the link.
90
        Path toolPath = getToolPath();
3✔
91
        FileAccess fileAccess = this.context.getFileAccess();
4✔
92
        if (Files.exists(toolPath, LinkOption.NOFOLLOW_LINKS)) {
9✔
93
          fileAccess.backup(toolPath);
4✔
94
        }
95
        fileAccess.mkdirs(toolPath.getParent());
4✔
96
        fileAccess.symlink(installation.linkDir(), toolPath);
5✔
97
      }
98
      this.context.getPath().setPath(this.tool, installation.binDir());
8✔
99
      postInstall(true);
3✔
100
      if (installedVersion == null) {
2!
101
        step.success("Successfully installed {} in version {}", this.tool, resolvedVersion);
15✔
102
      } else {
103
        step.success("Successfully installed {} in version {} replacing previous version {}", this.tool, resolvedVersion, installedVersion);
×
104
      }
105
      return true;
4✔
106
    } catch (RuntimeException e) {
1✔
107
      step.error(e, true);
4✔
108
      throw e;
2✔
109
    } finally {
110
      step.close();
2✔
111
    }
112

113
  }
114

115
  /**
116
   * This method is called after a tool was requested to be installed or updated.
117
   *
118
   * @param newlyInstalled {@code true} if the tool was installed or updated (at least link to software folder was created/updated), {@code false} otherwise
119
   *     (configured version was already installed and nothing changed).
120
   */
121
  protected void postInstall(boolean newlyInstalled) {
122

123
    if (newlyInstalled) {
2✔
124
      postInstall();
2✔
125
    }
126
  }
1✔
127

128
  /**
129
   * This method is called after the tool has been newly installed or updated to a new version.
130
   */
131
  protected void postInstall() {
132

133
    // nothing to do by default
134
  }
1✔
135

136
  private boolean toolAlreadyInstalled(boolean silent, VersionIdentifier installedVersion, Step step) {
137
    if (!silent) {
2✔
138
      this.context.info("Version {} of tool {} is already installed", installedVersion, getToolWithEdition());
15✔
139
    }
140
    postInstall(false);
3✔
141
    step.success();
2✔
142
    return false;
2✔
143
  }
144

145
  /**
146
   * Determines whether this tool should be installed directly in the software folder or in the software repository.
147
   *
148
   * @return {@code true} if the tool should be installed directly in the software folder, ignoring the central software repository; {@code false} if the tool
149
   *     should be installed in the central software repository (default behavior).
150
   */
151
  protected boolean isIgnoreSoftwareRepo() {
152

153
    return false;
2✔
154
  }
155

156
  /**
157
   * Performs the installation of the {@link #getName() tool} together with the environment context, managed by this
158
   * {@link com.devonfw.tools.ide.commandlet.Commandlet}.
159
   *
160
   * @param version the {@link GenericVersionRange} requested to be installed.
161
   * @param environmentContext the {@link EnvironmentContext} used to
162
   *     {@link #setEnvironment(EnvironmentContext, ToolInstallation, boolean) configure environment variables}.
163
   * @return the {@link ToolInstallation} matching the given {@code version}.
164
   */
165
  public ToolInstallation installTool(GenericVersionRange version, EnvironmentContext environmentContext) {
166

167
    return installTool(version, environmentContext, getConfiguredEdition());
7✔
168
  }
169

170
  /**
171
   * Performs the installation of the {@link #getName() tool} together with the environment context  managed by this
172
   * {@link com.devonfw.tools.ide.commandlet.Commandlet}.
173
   *
174
   * @param version the {@link GenericVersionRange} requested to be installed.
175
   * @param environmentContext the {@link EnvironmentContext} used to
176
   *     {@link #setEnvironment(EnvironmentContext, ToolInstallation, boolean) configure environment variables}.
177
   * @param edition the specific {@link #getConfiguredEdition() edition} to install.
178
   * @return the {@link ToolInstallation} matching the given {@code version}.
179
   */
180
  public ToolInstallation installTool(GenericVersionRange version, EnvironmentContext environmentContext, String edition) {
181

182
    // if version is a VersionRange, we are not called from install() but directly from installAsDependency() due to a version conflict of a dependency
183
    boolean extraInstallation = (version instanceof VersionRange);
3✔
184
    ToolRepository toolRepository = getToolRepository();
3✔
185
    VersionIdentifier resolvedVersion = toolRepository.resolveVersion(this.tool, edition, version, this);
8✔
186
    installToolDependencies(resolvedVersion, edition, environmentContext);
5✔
187

188
    Path installationPath;
189
    boolean ignoreSoftwareRepo = isIgnoreSoftwareRepo();
3✔
190
    if (ignoreSoftwareRepo) {
2✔
191
      installationPath = getToolPath();
4✔
192
    } else {
193
      Path softwareRepoPath = this.context.getSoftwareRepositoryPath().resolve(toolRepository.getId()).resolve(this.tool).resolve(edition);
12✔
194
      installationPath = softwareRepoPath.resolve(resolvedVersion.toString());
5✔
195
    }
196
    Path toolVersionFile = installationPath.resolve(IdeContext.FILE_SOFTWARE_VERSION);
4✔
197
    FileAccess fileAccess = this.context.getFileAccess();
4✔
198
    if (Files.isDirectory(installationPath)) {
5✔
199
      if (Files.exists(toolVersionFile)) {
5!
200
        if (!ignoreSoftwareRepo || resolvedVersion.equals(getInstalledVersion())) {
2!
201
          this.context.debug("Version {} of tool {} is already installed at {}", resolvedVersion, getToolWithEdition(this.tool, edition), installationPath);
21✔
202
          return createToolInstallation(installationPath, resolvedVersion, toolVersionFile, false, environmentContext, extraInstallation);
9✔
203
        }
204
      } else {
205
        this.context.warning("Deleting corrupted installation at {}", installationPath);
×
206
        fileAccess.delete(installationPath);
×
207
      }
208
    }
209
    Path downloadedToolFile = downloadTool(edition, toolRepository, resolvedVersion);
6✔
210
    boolean extract = isExtract();
3✔
211
    if (!extract) {
2✔
212
      this.context.trace("Extraction is disabled for '{}' hence just moving the downloaded file {}.", this.tool, downloadedToolFile);
15✔
213
    }
214
    if (Files.exists(installationPath)) {
5!
215
      fileAccess.backup(installationPath);
×
216
    }
217
    fileAccess.mkdirs(installationPath.getParent());
4✔
218
    fileAccess.extract(downloadedToolFile, installationPath, this::postExtract, extract);
7✔
219
    try {
220
      Files.writeString(toolVersionFile, resolvedVersion.toString(), StandardOpenOption.CREATE_NEW);
11✔
221
    } catch (IOException e) {
×
222
      throw new IllegalStateException("Failed to write version file " + toolVersionFile, e);
×
223
    }
1✔
224
    this.context.debug("Installed {} in version {} at {}", this.tool, resolvedVersion, installationPath);
19✔
225
    return createToolInstallation(installationPath, resolvedVersion, toolVersionFile, true, environmentContext, extraInstallation);
9✔
226
  }
227

228
  /**
229
   * @param edition the {@link #getConfiguredEdition() tool edition} to download.
230
   * @param toolRepository the {@link ToolRepository} to use.
231
   * @param resolvedVersion the resolved {@link VersionIdentifier version} to download.
232
   * @return the {@link Path} to the downloaded release file.
233
   */
234
  protected Path downloadTool(String edition, ToolRepository toolRepository, VersionIdentifier resolvedVersion) {
235
    return toolRepository.download(this.tool, edition, resolvedVersion, this);
8✔
236
  }
237

238
  /**
239
   * Install this tool as dependency of another tool.
240
   *
241
   * @param version the required {@link VersionRange}. See {@link ToolDependency#versionRange()}.
242
   * @param environmentContext the {@link EnvironmentContext}.
243
   * @return {@code true} if the tool was newly installed, {@code false} otherwise (installation was already present).
244
   */
245
  public boolean installAsDependency(VersionRange version, EnvironmentContext environmentContext) {
246

247
    VersionIdentifier configuredVersion = getConfiguredVersion();
3✔
248
    if (version.contains(configuredVersion)) {
4✔
249
      // prefer configured version if contained in version range
250
      return install(false, environmentContext);
5✔
251
    } else {
252
      if (isIgnoreSoftwareRepo()) {
3!
253
        throw new IllegalStateException(
×
254
            "Cannot satisfy dependency to " + this.tool + " in version " + version + " since it is conflicting with configured version " + configuredVersion
255
                + " and this tool does not support the software repository.");
256
      }
257
      this.context.info(
19✔
258
          "Configured version of tool {} is {} but does not match version to install {} - need to use different version from software repository.",
259
          this.tool, configuredVersion, version);
260
    }
261
    ToolInstallation toolInstallation = installTool(version, environmentContext);
5✔
262
    return toolInstallation.newInstallation();
3✔
263
  }
264

265
  private void installToolDependencies(VersionIdentifier version, String edition, EnvironmentContext environmentContext) {
266
    Collection<ToolDependency> dependencies = getToolRepository().findDependencies(this.tool, edition, version);
8✔
267
    String toolWithEdition = getToolWithEdition(this.tool, edition);
5✔
268
    int size = dependencies.size();
3✔
269
    this.context.debug("Tool {} has {} other tool(s) as dependency", toolWithEdition, size);
15✔
270
    for (ToolDependency dependency : dependencies) {
10✔
271
      this.context.trace("Ensuring dependency {} for tool {}", dependency.tool(), toolWithEdition);
15✔
272
      LocalToolCommandlet dependencyTool = this.context.getCommandletManager().getRequiredLocalToolCommandlet(dependency.tool());
7✔
273
      dependencyTool.installAsDependency(dependency.versionRange(), environmentContext);
6✔
274
    }
1✔
275
  }
1✔
276

277
  /**
278
   * Post-extraction hook that can be overridden to add custom processing after unpacking and before moving to the final destination folder.
279
   *
280
   * @param extractedDir the {@link Path} to the folder with the unpacked tool.
281
   */
282
  protected void postExtract(Path extractedDir) {
283

284
  }
1✔
285

286
  @Override
287
  public VersionIdentifier getInstalledVersion() {
288

289
    return getInstalledVersion(this.context.getSoftwarePath().resolve(getName()));
9✔
290
  }
291

292
  /**
293
   * @param toolPath the installation {@link Path} where to find the version file.
294
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
295
   */
296
  protected VersionIdentifier getInstalledVersion(Path toolPath) {
297

298
    if (!Files.isDirectory(toolPath)) {
5✔
299
      this.context.debug("Tool {} not installed in {}", getName(), toolPath);
15✔
300
      return null;
2✔
301
    }
302
    Path toolVersionFile = toolPath.resolve(IdeContext.FILE_SOFTWARE_VERSION);
4✔
303
    if (!Files.exists(toolVersionFile)) {
5✔
304
      Path legacyToolVersionFile = toolPath.resolve(IdeContext.FILE_LEGACY_SOFTWARE_VERSION);
4✔
305
      if (Files.exists(legacyToolVersionFile)) {
5✔
306
        toolVersionFile = legacyToolVersionFile;
3✔
307
      } else {
308
        this.context.warning("Tool {} is missing version file in {}", getName(), toolVersionFile);
15✔
309
        return null;
2✔
310
      }
311
    }
312
    String version = this.context.getFileAccess().readFileContent(toolVersionFile).trim();
7✔
313
    return VersionIdentifier.of(version);
3✔
314
  }
315

316
  @Override
317
  public String getInstalledEdition() {
318

319
    return getInstalledEdition(this.context.getSoftwarePath().resolve(this.tool));
9✔
320
  }
321

322
  /**
323
   * @param toolPath the installation {@link Path} where to find currently installed tool. The name of the parent directory of the real path corresponding
324
   *     to the passed {@link Path path} must be the name of the edition.
325
   * @return the installed edition of this tool or {@code null} if not installed.
326
   */
327
  private String getInstalledEdition(Path toolPath) {
328

329
    if (!Files.isDirectory(toolPath)) {
5✔
330
      this.context.debug("Tool {} not installed in {}", this.tool, toolPath);
15✔
331
      return null;
2✔
332
    }
333
    Path realPath = this.context.getFileAccess().toRealPath(toolPath);
6✔
334
    // if the realPath changed, a link has been resolved
335
    if (realPath.equals(toolPath)) {
4✔
336
      if (!isIgnoreSoftwareRepo()) {
3!
337
        this.context.warning("Tool {} is not installed via software repository (maybe from devonfw-ide). Please consider reinstalling it.", this.tool);
11✔
338
      }
339
      // I do not see any reliable way how we could determine the edition of a tool that does not use software repo or that was installed by devonfw-ide
340
      return getConfiguredEdition();
3✔
341
    }
342
    Path toolRepoFolder = context.getSoftwareRepositoryPath().resolve(ToolRepository.ID_DEFAULT).resolve(this.tool);
9✔
343
    String edition = getEdition(toolRepoFolder, realPath);
5✔
344
    if (!getToolRepository().getSortedEditions(this.tool).contains(edition)) {
8✔
345
      this.context.warning("Undefined edition {} of tool {}", edition, this.tool);
15✔
346
    }
347
    return edition;
2✔
348
  }
349

350
  private String getEdition(Path toolRepoFolder, Path toolInstallFolder) {
351

352
    int toolRepoNameCount = toolRepoFolder.getNameCount();
3✔
353
    int toolInstallNameCount = toolInstallFolder.getNameCount();
3✔
354
    if (toolRepoNameCount < toolInstallNameCount) {
3!
355
      // ensure toolInstallFolder starts with $IDE_ROOT/_ide/software/default/«tool»
356
      for (int i = 0; i < toolRepoNameCount; i++) {
7✔
357
        if (!toolRepoFolder.getName(i).toString().equals(toolInstallFolder.getName(i).toString())) {
10!
358
          return null;
×
359
        }
360
      }
361
      return toolInstallFolder.getName(toolRepoNameCount).toString();
5✔
362
    }
363
    return null;
×
364
  }
365

366
  @Override
367
  public void uninstall() {
368

369
    try {
370
      Path softwarePath = getToolPath();
3✔
371
      if (Files.exists(softwarePath)) {
5!
372
        try {
373
          this.context.getFileAccess().delete(softwarePath);
5✔
374
          this.context.success("Successfully uninstalled " + this.tool);
6✔
375
        } catch (Exception e) {
×
376
          this.context.error("Couldn't uninstall " + this.tool);
×
377
        }
1✔
378
      } else {
379
        this.context.warning("An installed version of " + this.tool + " does not exist");
×
380
      }
381
    } catch (Exception e) {
×
382
      this.context.error(e.getMessage());
×
383
    }
1✔
384
  }
1✔
385

386
  private ToolInstallation createToolInstallation(Path rootDir, VersionIdentifier resolvedVersion, Path toolVersionFile,
387
      boolean newInstallation, EnvironmentContext environmentContext, boolean extraInstallation) {
388

389
    Path linkDir = getMacOsHelper().findLinkDir(rootDir, getBinaryName());
7✔
390
    Path binDir = linkDir;
2✔
391
    Path binFolder = binDir.resolve(IdeContext.FOLDER_BIN);
4✔
392
    if (Files.isDirectory(binFolder)) {
5✔
393
      binDir = binFolder;
2✔
394
    }
395
    if (linkDir != rootDir) {
3✔
396
      assert (!linkDir.equals(rootDir));
5!
397
      this.context.getFileAccess().copy(toolVersionFile, linkDir, FileCopyMode.COPY_FILE_OVERRIDE);
7✔
398
    }
399
    ToolInstallation toolInstallation = new ToolInstallation(rootDir, linkDir, binDir, resolvedVersion, newInstallation);
9✔
400
    setEnvironment(environmentContext, toolInstallation, extraInstallation);
5✔
401
    return toolInstallation;
2✔
402
  }
403

404
  /**
405
   * Method to set environment variables for the process context.
406
   *
407
   * @param environmentContext the {@link EnvironmentContext} where to {@link EnvironmentContext#withEnvVar(String, String) set environment variables} for
408
   *     this tool.
409
   * @param toolInstallation the {@link ToolInstallation}.
410
   * @param extraInstallation {@code true} if the {@link ToolInstallation} is an additional installation to the
411
   *     {@link #getConfiguredVersion() configured version} due to a conflicting version of a {@link ToolDependency}, {@code false} otherwise.
412
   */
413
  public void setEnvironment(EnvironmentContext environmentContext, ToolInstallation toolInstallation, boolean extraInstallation) {
414

415
    String pathVariable = this.tool.toUpperCase(Locale.ROOT) + "_HOME";
6✔
416
    environmentContext.withEnvVar(pathVariable, toolInstallation.linkDir().toString());
7✔
417
    if (extraInstallation) {
2✔
418
      environmentContext.withPathEntry(toolInstallation.binDir());
5✔
419
    }
420
  }
1✔
421

422

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