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

devonfw / IDEasy / 10115741272

26 Jul 2024 06:12PM UTC coverage: 61.042% (-0.1%) from 61.148%
10115741272

Pull #453

github

web-flow
Merge a6cbea603 into 5a5c2abf0
Pull Request #453: #451: mac gatekeeper

2004 of 3613 branches covered (55.47%)

Branch coverage included in aggregate %.

5304 of 8359 relevant lines covered (63.45%)

2.8 hits per line

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

76.7
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.Path;
6
import java.nio.file.StandardOpenOption;
7
import java.util.HashMap;
8
import java.util.List;
9
import java.util.Set;
10

11
import com.devonfw.tools.ide.common.Tag;
12
import com.devonfw.tools.ide.context.AbstractIdeContext;
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.log.IdeLogLevel;
17
import com.devonfw.tools.ide.process.ProcessContext;
18
import com.devonfw.tools.ide.process.ProcessErrorHandling;
19
import com.devonfw.tools.ide.process.ProcessMode;
20
import com.devonfw.tools.ide.repo.ToolRepository;
21
import com.devonfw.tools.ide.step.Step;
22
import com.devonfw.tools.ide.url.model.file.dependencyJson.DependencyInfo;
23
import com.devonfw.tools.ide.version.VersionIdentifier;
24

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

30
  protected HashMap<String, String> dependenciesEnvVariableNames = null;
3✔
31

32
  protected HashMap<String, String> dependenciesEnvVariablePaths = new HashMap<>();
5✔
33

34
  private final Dependency dependency = new Dependency(this.context, this.tool);
9✔
35

36
  /**
37
   * The constructor.
38
   *
39
   * @param context the {@link IdeContext}.
40
   * @param tool the {@link #getName() tool name}.
41
   * @param tags the {@link #getTags() tags} classifying the tool. Should be created via {@link Set#of(Object) Set.of} method.
42
   */
43
  public LocalToolCommandlet(IdeContext context, String tool, Set<Tag> tags) {
44

45
    super(context, tool, tags);
5✔
46
  }
1✔
47

48
  /**
49
   * @return the {@link Path} where the tool is located (installed).
50
   */
51
  public Path getToolPath() {
52

53
    return this.context.getSoftwarePath().resolve(getName());
7✔
54
  }
55

56
  /**
57
   * @return the {@link Path} where the executables of the tool can be found. Typically a "bin" folder inside {@link #getToolPath() tool path}.
58
   */
59
  public Path getToolBinPath() {
60

61
    Path toolPath = getToolPath();
3✔
62
    Path binPath = this.context.getFileAccess().findFirst(toolPath, path -> path.getFileName().toString().equals("bin"), false);
14✔
63
    if ((binPath != null) && Files.isDirectory(binPath)) {
7!
64
      return binPath;
2✔
65
    }
66
    return toolPath;
2✔
67
  }
68

69
  @Override
70
  protected boolean doInstall(boolean silent) {
71

72
    VersionIdentifier configuredVersion = getConfiguredVersion();
3✔
73
    // get installed version before installInRepo actually may install the software
74
    VersionIdentifier installedVersion = getInstalledVersion();
3✔
75
    Step step = this.context.newStep(silent, "Install " + this.tool, configuredVersion);
14✔
76
    try {
77
      ToolRepository repository = this.context.getDefaultToolRepository();
4✔
78
      String edition = getConfiguredEdition();
3✔
79
      VersionIdentifier resolvedVersion = repository.resolveVersion(this.tool, edition, configuredVersion);
7✔
80
      if (resolvedVersion.equals(installedVersion)) {
4✔
81
        IdeLogLevel level = silent ? IdeLogLevel.DEBUG : IdeLogLevel.INFO;
6✔
82
        this.context.level(level).log("Version {} of tool {} is already installed", installedVersion, getToolWithEdition());
18✔
83
        step.success();
2✔
84
        return false;
4✔
85
      }
86
      // install configured version of our tool in the software repository if not already installed
87
      ToolInstallation installation = installInRepo(resolvedVersion, edition, repository);
6✔
88
      // we need to link the version or update the link.
89
      Path toolPath = getToolPath();
3✔
90
      FileAccess fileAccess = this.context.getFileAccess();
4✔
91
      if (Files.exists(toolPath)) {
5✔
92
        fileAccess.backup(toolPath);
3✔
93
      }
94
      fileAccess.mkdirs(toolPath.getParent());
4✔
95
      fileAccess.symlink(installation.linkDir(), toolPath);
5✔
96
      this.context.getPath().setPath(this.tool, installation.binDir());
8✔
97
      postInstall();
2✔
98
      if (installedVersion == null) {
2!
99
        step.success("Successfully installed {} in version {}", this.tool, resolvedVersion);
15✔
100
      } else {
101
        step.success("Successfully installed {} in version {} replacing previous version {}", this.tool, resolvedVersion, installedVersion);
×
102
      }
103
      return true;
4✔
104
    } catch (RuntimeException e) {
×
105
      step.error(e, true);
×
106
      throw e;
×
107
    } finally {
108
      step.close();
2✔
109
    }
110

111
  }
112

113
  /**
114
   * Performs the installation of the {@link #getName() tool} managed by this {@link com.devonfw.tools.ide.commandlet.Commandlet} only in the central software
115
   * repository without touching the IDE installation.
116
   *
117
   * @param version the {@link VersionIdentifier} requested to be installed. May also be a {@link VersionIdentifier#isPattern() version pattern}.
118
   * @return the {@link ToolInstallation} in the central software repository matching the given {@code version}.
119
   */
120
  public ToolInstallation installInRepo(VersionIdentifier version) {
121

122
    return installInRepo(version, getConfiguredEdition());
6✔
123
  }
124

125
  /**
126
   * Performs the installation of the {@link #getName() tool} managed by this {@link com.devonfw.tools.ide.commandlet.Commandlet} only in the central software
127
   * repository without touching the IDE installation.
128
   *
129
   * @param version the {@link VersionIdentifier} requested to be installed. May also be a {@link VersionIdentifier#isPattern() version pattern}.
130
   * @param edition the specific edition to install.
131
   * @return the {@link ToolInstallation} in the central software repository matching the given {@code version}.
132
   */
133
  public ToolInstallation installInRepo(VersionIdentifier version, String edition) {
134

135
    return installInRepo(version, edition, this.context.getDefaultToolRepository());
8✔
136
  }
137

138
  /**
139
   * Performs the installation of the {@link #getName() tool} managed by this {@link com.devonfw.tools.ide.commandlet.Commandlet} only in the central software
140
   * repository without touching the IDE installation.
141
   *
142
   * @param version the {@link VersionIdentifier} requested to be installed. May also be a {@link VersionIdentifier#isPattern() version pattern}.
143
   * @param edition the specific edition to install.
144
   * @param toolRepository the {@link ToolRepository} to use.
145
   * @return the {@link ToolInstallation} in the central software repository matching the given {@code version}.
146
   */
147
  public ToolInstallation installInRepo(VersionIdentifier version, String edition, ToolRepository toolRepository) {
148

149
    VersionIdentifier resolvedVersion = toolRepository.resolveVersion(this.tool, edition, version);
7✔
150

151
    if (Files.exists(this.dependency.getDependencyJsonPath(getConfiguredEdition()))) {
9✔
152
      installDependencies(resolvedVersion);
4✔
153
    } else {
154
      this.context.trace("No Dependencies file found");
4✔
155
    }
156

157
    Path toolPath = this.context.getSoftwareRepositoryPath().resolve(toolRepository.getId()).resolve(this.tool).resolve(edition)
12✔
158
        .resolve(resolvedVersion.toString());
3✔
159
    Path toolVersionFile = toolPath.resolve(IdeContext.FILE_SOFTWARE_VERSION);
4✔
160
    FileAccess fileAccess = this.context.getFileAccess();
4✔
161
    if (Files.isDirectory(toolPath)) {
5✔
162
      if (Files.exists(toolVersionFile)) {
5!
163
        this.context.debug("Version {} of tool {} is already installed at {}", resolvedVersion, getToolWithEdition(this.tool, edition), toolPath);
21✔
164
        return createToolInstallation(toolPath, resolvedVersion, toolVersionFile);
6✔
165
      } else {
166
        this.context.warning("Archiving corrupted installation at {}", toolPath);
×
167
        fileAccess.backup(toolPath);
×
168
      }
169
    }
170

171
    if (Files.exists(this.dependency.getDependencyJsonPath(getConfiguredEdition()))) {
9✔
172
      installDependencies(resolvedVersion);
4✔
173
    } else {
174
      this.context.trace("No Dependencies file found");
4✔
175
    }
176

177
    Path target = toolRepository.download(this.tool, edition, resolvedVersion);
7✔
178
    fileAccess.mkdirs(toolPath.getParent());
4✔
179
    boolean extract = isExtract();
3✔
180
    if (!extract) {
2✔
181
      this.context.trace("Extraction is disabled for '{}' hence just moving the downloaded file {}.", this.tool, target);
15✔
182
    }
183
    fileAccess.extract(target, toolPath, this::postExtract, extract);
7✔
184
    try {
185
      Files.writeString(toolVersionFile, resolvedVersion.toString(), StandardOpenOption.CREATE_NEW);
11✔
186
    } catch (IOException e) {
×
187
      throw new IllegalStateException("Failed to write version file " + toolVersionFile, e);
×
188
    }
1✔
189
    // newInstallation results in above conditions to be true if isForceMode is true or if the tool version file was
190
    // missing
191
    return createToolInstallation(toolPath, resolvedVersion, toolVersionFile, true);
7✔
192
  }
193

194
  /**
195
   * Post-extraction hook that can be overridden to add custom processing after unpacking and before moving to the final destination folder.
196
   *
197
   * @param extractedDir the {@link Path} to the folder with the unpacked tool.
198
   */
199
  protected void postExtract(Path extractedDir) {
200

201
  }
1✔
202

203
  @Override
204
  public VersionIdentifier getInstalledVersion() {
205

206
    return getInstalledVersion(this.context.getSoftwarePath().resolve(getName()));
9✔
207
  }
208

209
  /**
210
   * @param toolPath the installation {@link Path} where to find the version file.
211
   * @return the currently installed {@link VersionIdentifier version} of this tool or {@code null} if not installed.
212
   */
213
  protected VersionIdentifier getInstalledVersion(Path toolPath) {
214

215
    if (!Files.isDirectory(toolPath)) {
5✔
216
      this.context.debug("Tool {} not installed in {}", getName(), toolPath);
15✔
217
      return null;
2✔
218
    }
219
    Path toolVersionFile = toolPath.resolve(IdeContext.FILE_SOFTWARE_VERSION);
4✔
220
    if (!Files.exists(toolVersionFile)) {
5✔
221
      Path legacyToolVersionFile = toolPath.resolve(IdeContext.FILE_LEGACY_SOFTWARE_VERSION);
4✔
222
      if (Files.exists(legacyToolVersionFile)) {
5✔
223
        toolVersionFile = legacyToolVersionFile;
3✔
224
      } else {
225
        this.context.warning("Tool {} is missing version file in {}", getName(), toolVersionFile);
15✔
226
        return null;
2✔
227
      }
228
    }
229
    try {
230
      String version = Files.readString(toolVersionFile).trim();
4✔
231
      return VersionIdentifier.of(version);
3✔
232
    } catch (IOException e) {
×
233
      throw new IllegalStateException("Failed to read file " + toolVersionFile, e);
×
234
    }
235
  }
236

237
  @Override
238
  public String getInstalledEdition() {
239

240
    return getInstalledEdition(this.context.getSoftwarePath().resolve(getName()));
9✔
241
  }
242

243
  /**
244
   * @param toolPath the installation {@link Path} where to find currently installed tool. The name of the parent directory of the real path corresponding to
245
   * the passed {@link Path path} must be the name of the edition.
246
   * @return the installed edition of this tool or {@code null} if not installed.
247
   */
248
  public String getInstalledEdition(Path toolPath) {
249

250
    if (!Files.isDirectory(toolPath)) {
5!
251
      this.context.debug("Tool {} not installed in {}", getName(), toolPath);
×
252
      return null;
×
253
    }
254
    try {
255
      String edition = toolPath.toRealPath().getParent().getFileName().toString();
8✔
256
      if (!this.context.getUrls().getSortedEditions(getName()).contains(edition)) {
9!
257
        edition = getConfiguredEdition();
3✔
258
      }
259
      return edition;
2✔
260
    } catch (IOException e) {
×
261
      throw new IllegalStateException(
×
262
          "Couldn't determine the edition of " + getName() + " from the directory structure of its software path "
×
263
              + toolPath
264
              + ", assuming the name of the parent directory of the real path of the software path to be the edition "
265
              + "of the tool.", e);
266
    }
267

268
  }
269

270
  @Override
271
  public void uninstall() {
272

273
    try {
274
      Path softwarePath = getToolPath();
3✔
275
      if (Files.exists(softwarePath)) {
5✔
276
        try {
277
          this.context.getFileAccess().delete(softwarePath);
5✔
278
          this.context.success("Successfully uninstalled " + this.tool);
6✔
279
        } catch (Exception e) {
1✔
280
          this.context.error("Couldn't uninstall " + this.tool);
6✔
281
        }
2✔
282
      } else {
283
        this.context.warning("An installed version of " + this.tool + " does not exist");
6✔
284
      }
285
    } catch (Exception e) {
×
286
      this.context.error(e.getMessage());
×
287
    }
1✔
288
  }
1✔
289

290
  private ToolInstallation createToolInstallation(Path rootDir, VersionIdentifier resolvedVersion, Path toolVersionFile,
291
      boolean newInstallation) {
292

293
    Path linkDir = getMacOsHelper().findLinkDir(rootDir, getBinaryName());
7✔
294
    Path binDir = linkDir;
2✔
295
    Path binFolder = binDir.resolve(IdeContext.FOLDER_BIN);
4✔
296
    if (Files.isDirectory(binFolder)) {
5✔
297
      binDir = binFolder;
2✔
298
    }
299
    if (newInstallation && (linkDir != rootDir)) {
5✔
300
      assert (!linkDir.equals(rootDir));
5!
301
      this.context.getFileAccess().copy(toolVersionFile, linkDir, FileCopyMode.COPY_FILE_OVERRIDE);
7✔
302
      if (this.context.getSystemInfo().isMac() && !((AbstractIdeContext) this.context).isTest()) {
10!
303
        Path macApp = findMacApp(linkDir);
×
304
        if (macApp != null) {
×
305
          ProcessContext pc = this.context.newProcess();
×
306
          pc.executable("sudo").addArgs("xattr", "-d", "com.apple.quarantine", macApp);
×
307
          pc.run();
×
308
        }
309
      }
310
    }
311
    return new ToolInstallation(rootDir, linkDir, binDir, resolvedVersion, newInstallation);
9✔
312
  }
313

314
  private Path findMacApp(Path path) {
315

316
    while (path != null) {
×
317
      Path fileName = path.getFileName();
×
318
      if (fileName == null) {
×
319
        return null;
×
320
      }
321
      String filename = fileName.toString();
×
322
      if (filename.endsWith(".app")) {
×
323
        return path;
×
324
      } else if (filename.equals(IdeContext.FOLDER_CONTENTS)) {
×
325
        return path.getParent();
×
326
      }
327
      path = path.getParent();
×
328
    }
×
329
    return null;
×
330
  }
331

332
  private ToolInstallation createToolInstallation(Path rootDir, VersionIdentifier resolvedVersion, Path toolVersionFile) {
333

334
    return createToolInstallation(rootDir, resolvedVersion, toolVersionFile, false);
7✔
335
  }
336

337
  @Override
338
  public void runTool(ProcessMode processMode, VersionIdentifier toolVersion, String... args) {
339

340
    Path binaryPath;
341
    Path toolPath = Path.of(getBinaryName());
6✔
342
    if (toolVersion == null) {
2!
343
      install(true);
4✔
344
      binaryPath = toolPath;
3✔
345
    } else {
346
      throw new UnsupportedOperationException("Not yet implemented!");
×
347
    }
348

349
    if (Files.exists(this.dependency.getDependencyJsonPath(getConfiguredEdition()))) {
9✔
350
      setDependencyRepository(getInstalledVersion());
5✔
351
    } else {
352
      this.context.trace("No Dependencies file found");
4✔
353
    }
354

355
    ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.WARNING).executable(binaryPath).addArgs(args);
10✔
356

357
    for (String key : this.dependenciesEnvVariablePaths.keySet()) {
12✔
358

359
      String dependencyPath = this.dependenciesEnvVariablePaths.get(key);
6✔
360
      pc = pc.withEnvVar(key, dependencyPath);
5✔
361
    }
1✔
362

363
    pc.run(processMode);
4✔
364
  }
1✔
365

366
  private void installDependencies(VersionIdentifier version) {
367

368
    List<DependencyInfo> dependencies = this.dependency.readJson(version, getConfiguredEdition());
7✔
369

370
    for (DependencyInfo dependencyInfo : dependencies) {
10✔
371

372
      String dependencyName = dependencyInfo.getTool();
3✔
373
      VersionIdentifier dependencyVersionToInstall = this.dependency.findDependencyVersionToInstall(dependencyInfo);
5✔
374
      if (dependencyVersionToInstall == null) {
2!
375
        continue;
×
376
      }
377

378
      ToolCommandlet dependencyTool = this.context.getCommandletManager().getToolCommandlet(dependencyName);
6✔
379
      Path dependencyRepository = getDependencySoftwareRepository(dependencyName,
5✔
380
          dependencyTool.getConfiguredEdition());
1✔
381

382
      if (!Files.exists(dependencyRepository)) {
5!
383
        installDependencyInRepo(dependencyName, dependencyTool, dependencyVersionToInstall);
×
384
      } else {
385
        Path versionExistingInRepository = this.dependency.versionExistsInRepository(dependencyRepository, dependencyInfo.getVersionRange());
7✔
386
        if (versionExistingInRepository.equals(Path.of(""))) {
7✔
387
          installDependencyInRepo(dependencyName, dependencyTool, dependencyVersionToInstall);
6✔
388
        } else {
389
          this.context.info("Necessary version of the dependency {} is already installed in repository", dependencyName);
10✔
390
        }
391
      }
392
    }
1✔
393
  }
1✔
394

395
  private void installDependencyInRepo(String dependencyName, ToolCommandlet dependencyTool, VersionIdentifier dependencyVersionToInstall) {
396

397
    this.context.info("The version {} of the dependency {} is being installed", dependencyVersionToInstall, dependencyName);
14✔
398
    LocalToolCommandlet dependencyLocal = (LocalToolCommandlet) dependencyTool;
3✔
399
    dependencyLocal.installInRepo(dependencyVersionToInstall);
4✔
400
    this.context.info("The version {} of the dependency {} was successfully installed", dependencyVersionToInstall, dependencyName);
14✔
401
  }
1✔
402

403
  protected void setDependencyRepository(VersionIdentifier version) {
404

405
    List<DependencyInfo> dependencies = this.dependency.readJson(version, getConfiguredEdition());
7✔
406

407
    for (DependencyInfo dependencyInfo : dependencies) {
10✔
408
      String dependencyName = dependencyInfo.getTool();
3✔
409
      VersionIdentifier dependencyVersionToInstall = this.dependency.findDependencyVersionToInstall(dependencyInfo);
5✔
410
      if (dependencyVersionToInstall == null) {
2!
411
        continue;
×
412
      }
413

414
      ToolCommandlet dependencyTool = this.context.getCommandletManager().getToolCommandlet(dependencyName);
6✔
415
      Path dependencyRepository = getDependencySoftwareRepository(dependencyName,
5✔
416
          dependencyTool.getConfiguredEdition());
1✔
417
      Path versionExistingInRepository = this.dependency.versionExistsInRepository(dependencyRepository,
6✔
418
          dependencyInfo.getVersionRange());
1✔
419
      Path dependencyPath;
420

421
      if (versionExistingInRepository.equals(Path.of(""))) {
7!
422
        dependencyPath = dependencyRepository.resolve(dependencyVersionToInstall.toString());
×
423
      } else {
424
        dependencyPath = dependencyRepository.resolve(versionExistingInRepository);
4✔
425
      }
426
      setDependencyEnvironmentPath(getDependencyEnvironmentName(dependencyName), dependencyPath);
6✔
427
    }
1✔
428
  }
1✔
429

430
  private void setDependencyEnvironmentPath(String dependencyEnvironmentName, Path dependencyPath) {
431

432
    this.dependenciesEnvVariablePaths.put(dependencyEnvironmentName, dependencyPath.toString());
7✔
433

434
  }
1✔
435

436
  /**
437
   * Method to return the list of the environment variable name for the dependencies. If necessary, it should be overridden in the specific tool
438
   *
439
   * @return the {@link HashMap} with the dependency name mapped to the env variable, for example ( java: JAVA_HOME )
440
   */
441

442
  protected HashMap<String, String> listOfDependencyEnvVariableNames() {
443

444
    return this.dependenciesEnvVariableNames;
×
445
  }
446

447
  private String getDependencyEnvironmentName(String dependencyName) {
448

449
    HashMap<String, String> envVariableName = listOfDependencyEnvVariableNames();
3✔
450

451
    if (envVariableName != null) {
2!
452
      return envVariableName.get(dependencyName);
5✔
453
    }
454

455
    return dependencyName.toUpperCase() + "_HOME";
×
456
  }
457

458
  private Path getDependencySoftwareRepository(String dependencyName, String dependencyEdition) {
459

460
    String defaultToolRepositoryId = this.context.getDefaultToolRepository().getId();
5✔
461
    Path dependencyRepository = this.context.getSoftwareRepositoryPath().resolve(defaultToolRepositoryId).resolve(dependencyName).resolve(dependencyEdition);
10✔
462

463
    return dependencyRepository;
2✔
464
  }
465

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