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

devonfw / IDEasy / 29098761063

10 Jul 2026 02:09PM UTC coverage: 72.274% (+0.05%) from 72.225%
29098761063

Pull #2149

github

web-flow
Merge de9c5a9d9 into 365f379a1
Pull Request #2149: #2102 set ideRoot during msi installation correctly

4885 of 7458 branches covered (65.5%)

Branch coverage included in aggregate %.

12583 of 16711 relevant lines covered (75.3%)

3.19 hits per line

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

74.76
cli/src/main/java/com/devonfw/tools/ide/tool/IdeasyCommandlet.java
1
package com.devonfw.tools.ide.tool;
2

3
import java.io.Reader;
4
import java.io.Writer;
5
import java.nio.file.Files;
6
import java.nio.file.Path;
7
import java.util.ArrayList;
8
import java.util.Iterator;
9
import java.util.List;
10
import java.util.Map;
11
import java.util.Objects;
12
import java.util.Set;
13
import java.util.regex.Matcher;
14
import java.util.regex.Pattern;
15

16
import org.slf4j.Logger;
17
import org.slf4j.LoggerFactory;
18

19
import com.devonfw.tools.ide.cli.CliException;
20
import com.devonfw.tools.ide.commandlet.UpgradeMode;
21
import com.devonfw.tools.ide.common.SimpleSystemPath;
22
import com.devonfw.tools.ide.common.Tag;
23
import com.devonfw.tools.ide.context.IdeContext;
24
import com.devonfw.tools.ide.io.FileAccess;
25
import com.devonfw.tools.ide.io.ini.IniFile;
26
import com.devonfw.tools.ide.io.ini.IniSection;
27
import com.devonfw.tools.ide.log.IdeLogLevel;
28
import com.devonfw.tools.ide.os.WindowsHelper;
29
import com.devonfw.tools.ide.os.WindowsPathSyntax;
30
import com.devonfw.tools.ide.process.ProcessMode;
31
import com.devonfw.tools.ide.process.ProcessResult;
32
import com.devonfw.tools.ide.tool.mvn.MvnArtifact;
33
import com.devonfw.tools.ide.tool.mvn.MvnBasedLocalToolCommandlet;
34
import com.devonfw.tools.ide.tool.mvn.MvnRepository;
35
import com.devonfw.tools.ide.variable.IdeVariables;
36
import com.devonfw.tools.ide.version.IdeVersion;
37
import com.devonfw.tools.ide.version.VersionIdentifier;
38
import com.fasterxml.jackson.databind.JsonNode;
39
import com.fasterxml.jackson.databind.ObjectMapper;
40
import com.fasterxml.jackson.databind.node.ArrayNode;
41
import com.fasterxml.jackson.databind.node.ObjectNode;
42

43
/**
44
 * {@link MvnBasedLocalToolCommandlet} for IDEasy (ide-cli).
45
 */
46
public class IdeasyCommandlet extends MvnBasedLocalToolCommandlet {
47

48
  private static final Logger LOG = LoggerFactory.getLogger(IdeasyCommandlet.class);
3✔
49

50
  /** The {@link MvnArtifact} for IDEasy. */
51
  public static final MvnArtifact ARTIFACT = MvnArtifact.ofIdeasyCli("*!", "tar.gz", "${os}-${arch}");
5✔
52

53
  private static final String BASH_CODE_SOURCE_FUNCTIONS = "source \"$IDE_ROOT/_ide/installation/functions\"";
54

55
  /** The {@link #getName() tool name}. */
56
  public static final String TOOL_NAME = "ideasy";
57
  public static final String BASHRC = ".bashrc";
58
  public static final String ZSHRC = ".zshrc";
59
  public static final String IDE_BIN = "\\_ide\\bin";
60
  public static final String IDE_INSTALLATION_BIN = "\\_ide\\installation\\bin";
61

62

63
  private static final Map<String, Boolean> REQUIRED_INSTALLATION_ARTIFACTS = Map.of(
4✔
64
      //artifactName: String, required: boolean
65
      "bin", true,
3✔
66
      "functions", true,
3✔
67
      "internal", true,
3✔
68
      "gui", true,
3✔
69
      "system", true,
3✔
70
      "IDEasy.pdf", true,
3✔
71
      "setup", true,
3✔
72
      "setup.bat", false
1✔
73
  );
74

75
  private final UpgradeMode mode;
76

77
  /** Pattern for IDEasy SNAPSHOT versions built locally. */
78
  // ..............................................................................1..........................2........3........4
79
  private static final Pattern PATTERN_IDEASY_SNAPSHOT_VERSION = Pattern.compile("^(\\d{4}\\.\\d{2}\\.\\d{3})-(\\d{2})_(\\d{2})_(\\d{2}).*-SNAPSHOT$");
3✔
80

81
  /** Pattern for Maven/Nexus SNAPSHOT versions from downloads . */
82
  // .............................................................................1..........................2.......3.......4..........5
83
  private static final Pattern PATTERN_MAVEN_SNAPSHOT_VERSION = Pattern.compile("^(\\d{4}\\.\\d{2}\\.\\d{3})-(\\d{4})(\\d{2})(\\d{2})\\.(\\d{2})\\d{4}.*$");
4✔
84

85
  // Group numbers for PATTERN_IDEASY_SNAPSHOT_VERSION
86
  private static final int GROUP_IDEASY_BASE = 1;
87
  private static final int GROUP_IDEASY_MONTH = 2;
88
  private static final int GROUP_IDEASY_DAY = 3;
89
  private static final int GROUP_IDEASY_HOUR = 4;
90

91
  // Group numbers for PATTERN_MAVEN_SNAPSHOT_VERSION
92
  private static final int GROUP_MAVEN_BASE = 1;
93
  private static final int GROUP_MAVEN_YEAR = 2;
94
  private static final int GROUP_MAVEN_MONTH = 3;
95
  private static final int GROUP_MAVEN_DAY = 4;
96
  private static final int GROUP_MAVEN_HOUR = 5;
97

98
  /**
99
   * The constructor.
100
   *
101
   * @param context the {@link IdeContext}.
102
   */
103
  public IdeasyCommandlet(IdeContext context) {
104
    this(context, UpgradeMode.STABLE);
4✔
105
  }
1✔
106

107
  /**
108
   * The constructor.
109
   *
110
   * @param context the {@link IdeContext}.
111
   * @param mode the {@link UpgradeMode}.
112
   */
113
  public IdeasyCommandlet(IdeContext context, UpgradeMode mode) {
114

115
    super(context, TOOL_NAME, ARTIFACT, Set.of(Tag.PRODUCTIVITY, Tag.IDE));
8✔
116
    this.mode = mode;
3✔
117
  }
1✔
118

119
  @Override
120
  public VersionIdentifier getInstalledVersion() {
121

122
    return IdeVersion.getVersionIdentifier();
2✔
123
  }
124

125
  @Override
126
  public String getInstalledEdition() {
127

128
    return this.tool;
3✔
129
  }
130

131
  @Override
132
  public String getConfiguredEdition() {
133

134
    return this.tool;
3✔
135
  }
136

137
  @Override
138
  public VersionIdentifier getConfiguredVersion() {
139

140
    UpgradeMode upgradeMode = this.mode;
3✔
141
    if (upgradeMode == null) {
2!
142
      if (IdeVersion.isSnapshot()) {
2✔
143
        upgradeMode = UpgradeMode.SNAPSHOT;
3✔
144
      } else {
145
        if (IdeVersion.getVersionIdentifier().getDevelopmentPhase().isStable()) {
4!
146
          upgradeMode = UpgradeMode.STABLE;
3✔
147
        } else {
148
          upgradeMode = UpgradeMode.UNSTABLE;
×
149
        }
150
      }
151
    }
152
    return upgradeMode.getVersion();
3✔
153
  }
154

155
  @Override
156
  public Path getToolPath() {
157

158
    return this.context.getIdeInstallationPath();
4✔
159
  }
160

161
  @Override
162
  protected ToolInstallation doInstall(ToolInstallRequest request) {
163

164
    this.context.requireOnline("upgrade of IDEasy", true);
5✔
165

166
    if (IdeVersion.isUndefined() && !this.context.isForceMode()) {
6!
167
      VersionIdentifier version = IdeVersion.getVersionIdentifier();
2✔
168
      LOG.warn("You are using IDEasy version {} which indicates local development - skipping upgrade.", version);
4✔
169
      return toolAlreadyInstalled(request);
4✔
170
    }
171
    return super.doInstall(request);
×
172
  }
173

174
  /**
175
   * @return the latest released {@link VersionIdentifier version} of IDEasy.
176
   */
177
  public VersionIdentifier getLatestVersion() {
178

179
    if (!this.context.isForceMode()) {
4!
180
      VersionIdentifier currentVersion = IdeVersion.getVersionIdentifier();
2✔
181
      if (IdeVersion.isUndefined()) {
2✔
182
        return currentVersion;
2✔
183
      }
184
    }
185
    VersionIdentifier configuredVersion = getConfiguredVersion();
3✔
186
    return getToolRepository().resolveVersion(this.tool, getConfiguredEdition(), configuredVersion, this);
10✔
187
  }
188

189
  /**
190
   * Checks if an update is available and logs according information.
191
   *
192
   * @return {@code true} if an update is available, {@code false} otherwise.
193
   */
194
  public boolean checkIfUpdateIsAvailable() {
195
    VersionIdentifier installedVersion = getInstalledVersion();
3✔
196
    IdeLogLevel.SUCCESS.log(LOG, "Your version of IDEasy is {}.", installedVersion);
10✔
197
    if (IdeVersion.isSnapshot()) {
2✔
198
      LOG.warn("You are using a SNAPSHOT version of IDEasy. For stability consider switching to a stable release via 'ide upgrade --mode=stable'");
3✔
199
    }
200
    if (this.context.isOffline()) {
4✔
201
      LOG.warn("Skipping check for newer version of IDEasy because you are offline.");
3✔
202
      return false;
2✔
203
    }
204
    VersionIdentifier latestVersion = getLatestVersion();
3✔
205
    if (IdeVersion.isSnapshot()) {
2✔
206
      if (isSameSnapshotVersion(installedVersion.toString(), latestVersion.toString())) {
7✔
207
        IdeLogLevel.SUCCESS.log(LOG, "Your are using the latest snapshot version of IDEasy and no update is available.");
4✔
208
        return false;
2✔
209
      }
210
    } else if (installedVersion.equals(latestVersion)) {
4!
211
      IdeLogLevel.SUCCESS.log(LOG, "Your are using the latest stable version of IDEasy and no update is available.");
×
212
      return false;
×
213
    }
214
    IdeLogLevel.INTERACTION.log(LOG,
14✔
215
        "Your version of IDEasy is {} but version {} is available. Please run the following command to upgrade to the latest version:\n"
216
            + "ide upgrade", installedVersion, latestVersion);
217
    return true;
2✔
218
  }
219

220
  /**
221
   * Checks if two snapshot versions represent the version
222
   *
223
   * @param installed the installed version string
224
   * @param latest the latest available version string
225
   * @return {@code true} if both versions represent the same version, {@code false} otherwise.
226
   */
227
  private boolean isSameSnapshotVersion(String installed, String latest) {
228
    if (installed == null || latest == null) {
4!
229
      return false;
×
230
    }
231

232
    Matcher installedMatcher = PATTERN_IDEASY_SNAPSHOT_VERSION.matcher(installed);
4✔
233
    Matcher latestMatcher = PATTERN_MAVEN_SNAPSHOT_VERSION.matcher(latest);
4✔
234

235
    if (!installedMatcher.matches() || !latestMatcher.matches()) {
6!
236
      return false;
2✔
237
    }
238

239
    // Compare base versions
240
    String baseInstalled = installedMatcher.group(GROUP_IDEASY_BASE);
4✔
241
    String baseLatest = latestMatcher.group(GROUP_MAVEN_BASE);
4✔
242
    if (!baseInstalled.equals(baseLatest)) {
4✔
243
      return false;
2✔
244
    }
245

246
    // Compare year
247
    String yearLatest = latestMatcher.group(GROUP_MAVEN_YEAR);
4✔
248
    String baseYear = baseInstalled.split("\\.")[0];
6✔
249
    if (!baseYear.equals(yearLatest)) {
4!
250
      return false;
×
251
    }
252

253
    // Compare MMDD.HH for both versions
254
    String keyInstalled =
2✔
255
        installedMatcher.group(GROUP_IDEASY_MONTH) + installedMatcher.group(GROUP_IDEASY_DAY) + "." + installedMatcher.group(GROUP_IDEASY_HOUR);
9✔
256
    String keyLatest = latestMatcher.group(GROUP_MAVEN_MONTH) + latestMatcher.group(GROUP_MAVEN_DAY) + "." + latestMatcher.group(GROUP_MAVEN_HOUR);
11✔
257

258
    return keyInstalled.equals(keyLatest);
4✔
259
  }
260

261
  /**
262
   * Initial installation of IDEasy.
263
   *
264
   * @param cwd the {@link Path} to the current working directory.
265
   * @see com.devonfw.tools.ide.commandlet.InstallCommandlet
266
   */
267
  public void installIdeasy(Path cwd) {
268
    Path ideRoot = determineIdeRoot(cwd);
4✔
269
    // During a fresh (MSI) installation the IDE_ROOT environment variable is not yet available, so context.getIdeRoot() would return null for the whole run.
270
    // The install target is already known here, so we make the context consistent for any downstream code reading context.getIdeRoot() (see #1517).
271
    this.context.setIdeRoot(ideRoot);
4✔
272
    Path idePath = ideRoot.resolve(IdeContext.FOLDER_UNDERSCORE_IDE);
4✔
273
    Path installationPath = idePath.resolve(IdeContext.FOLDER_INSTALLATION);
4✔
274
    Path ideasySoftwarePath = idePath.resolve(IdeContext.FOLDER_SOFTWARE).resolve(MvnRepository.ID).resolve(IdeasyCommandlet.TOOL_NAME)
8✔
275
        .resolve(IdeasyCommandlet.TOOL_NAME);
2✔
276
    Path ideasyVersionPath = ideasySoftwarePath.resolve(IdeVersion.getVersionString());
4✔
277
    FileAccess fileAccess = this.context.getFileAccess();
4✔
278
    if (Files.isDirectory(ideasyVersionPath)) {
5✔
279
      LOG.error("IDEasy is already installed at {} - if your installation is broken, delete it manually and rerun setup!", ideasyVersionPath);
5✔
280
    } else {
281
      List<Path> installationArtifacts = new ArrayList<>();
4✔
282
      for (Map.Entry<String, Boolean> artifactEntry : REQUIRED_INSTALLATION_ARTIFACTS.entrySet()) {
11✔
283
        String artifactName = artifactEntry.getKey();
4✔
284
        boolean required = artifactEntry.getValue();
5✔
285
        boolean success = addInstallationArtifact(cwd, artifactName, required, installationArtifacts);
7✔
286
        if (!success) {
2!
287
          throw new CliException("IDEasy release is inconsistent at %s [artifact=%s]".formatted(cwd, artifactName));
×
288
        }
289
      }
1✔
290

291
      fileAccess.mkdirs(ideasyVersionPath);
3✔
292
      for (Path installationArtifact : installationArtifacts) {
10✔
293
        fileAccess.copy(installationArtifact, ideasyVersionPath);
4✔
294
      }
1✔
295
      this.context.writeVersionFile(IdeVersion.getVersionIdentifier(), ideasyVersionPath);
5✔
296
    }
297
    fileAccess.symlink(ideasyVersionPath, installationPath);
4✔
298
    addToShellRc(BASHRC, ideRoot, null);
5✔
299
    addToShellRc(ZSHRC, ideRoot, "autoload -U +X bashcompinit && bashcompinit");
5✔
300
    installIdeasyWindowsEnv(ideRoot, installationPath);
4✔
301
    IdeLogLevel.SUCCESS.log(LOG, "IDEasy has been installed successfully on your system.");
4✔
302
    LOG.warn("IDEasy has been setup for new shells but it cannot work in your current shell(s).\n"
3✔
303
        + "To use it here, run 'source ~/.bashrc' (or your shell config). Otherwise, open a new terminal or reboot.");
304
  }
1✔
305

306
  private void installIdeasyWindowsEnv(Path ideRoot, Path installationPath) {
307
    if (!this.context.getSystemInfo().isWindows()) {
5✔
308
      return;
1✔
309
    }
310
    WindowsHelper helper = WindowsHelper.get(this.context);
4✔
311
    helper.setUserEnvironmentValue(IdeVariables.IDE_ROOT.getName(), ideRoot.toString());
6✔
312
    String userPath = helper.getUserEnvironmentValue(IdeVariables.PATH.getName());
5✔
313
    if (userPath == null) {
2!
314
      LOG.error("Could not read user PATH from registry!");
×
315
    } else {
316
      LOG.info("Found user PATH={}", userPath);
4✔
317
      Path ideasyBinPath = installationPath.resolve("bin");
4✔
318
      SimpleSystemPath path = SimpleSystemPath.of(userPath, ';');
4✔
319
      if (path.getEntries().isEmpty()) {
4!
320
        LOG.warn("ATTENTION:\n"
×
321
            + "Your user specific PATH variable seems to be empty.\n"
322
            + "You can double check this by pressing [Windows][r] and launch the program SystemPropertiesAdvanced.\n"
323
            + "Then click on 'Environment variables' and check if 'PATH' is set in the 'user variables' from the upper list.\n"
324
            + "In case 'PATH' is defined there non-empty and you get this message, please abort and give us feedback:\n"
325
            + "https://github.com/devonfw/IDEasy/issues\n"
326
            + "Otherwise all is correct and you can continue.");
327
        this.context.askToContinue("Are you sure you want to override your PATH?");
×
328
      } else {
329
        path.removeEntries(s -> s.endsWith(IDE_INSTALLATION_BIN));
7✔
330
      }
331
      path.getEntries().add(ideasyBinPath.toString());
6✔
332
      helper.setUserEnvironmentValue(IdeVariables.PATH.getName(), path.toString());
6✔
333
      setGitLongpaths();
2✔
334
    }
335
  }
1✔
336

337
  private void setGitLongpaths() {
338
    this.context.getGitContext().findGitRequired();
5✔
339
    Path configPath = this.context.getUserHome().resolve(".gitconfig");
6✔
340
    FileAccess fileAccess = this.context.getFileAccess();
4✔
341
    IniFile iniFile = fileAccess.readIniFile(configPath);
4✔
342
    IniSection coreSection = iniFile.getOrCreateSection("core");
4✔
343
    coreSection.setProperty("longpaths", "true");
4✔
344
    fileAccess.writeIniFile(iniFile, configPath);
4✔
345
  }
1✔
346

347
  /**
348
   * Sets up Windows Terminal with Git Bash integration.
349
   */
350
  public void setupWindowsTerminal() {
351
    if (!this.context.getSystemInfo().isWindows()) {
×
352
      return;
×
353
    }
354
    if (!isWindowsTerminalInstalled()) {
×
355
      try {
356
        installWindowsTerminal();
×
357
      } catch (Exception e) {
×
358
        LOG.error("Failed to install Windows Terminal!", e);
×
359
      }
×
360
    }
361
    configureWindowsTerminalGitBash();
×
362
  }
×
363

364
  /**
365
   * Checks if Windows Terminal is installed.
366
   *
367
   * @return {@code true} if Windows Terminal is installed, {@code false} otherwise.
368
   */
369
  private boolean isWindowsTerminalInstalled() {
370
    try {
371
      ProcessResult result = this.context.newProcess()
×
372
          .executable("powershell")
×
373
          .addArgs("-Command", "Get-AppxPackage -Name Microsoft.WindowsTerminal")
×
374
          .run(ProcessMode.DEFAULT_CAPTURE);
×
375
      return result.isSuccessful() && !result.getOut().isEmpty();
×
376
    } catch (Exception e) {
×
377
      LOG.debug("Failed to check Windows Terminal installation.", e);
×
378
      return false;
×
379
    }
380
  }
381

382
  /**
383
   * Installs Windows Terminal using winget.
384
   */
385
  private void installWindowsTerminal() {
386
    try {
387
      LOG.info("Installing Windows Terminal...");
×
388
      ProcessResult result = this.context.newProcess()
×
389
          .executable("winget")
×
390
          .addArgs("install", "Microsoft.WindowsTerminal")
×
391
          .run(ProcessMode.DEFAULT);
×
392
      if (result.isSuccessful()) {
×
393
        IdeLogLevel.SUCCESS.log(LOG, "Windows Terminal has been installed successfully.");
×
394
      } else {
395
        LOG.warn("Failed to install Windows Terminal. Please install it manually from Microsoft Store.");
×
396
      }
397
    } catch (Exception e) {
×
398
      LOG.warn("Failed to install Windows Terminal: {}. Please install it manually from Microsoft Store.", e.toString());
×
399
    }
×
400
  }
×
401

402
  /**
403
   * Configures Git Bash integration in Windows Terminal.
404
   */
405
  protected void configureWindowsTerminalGitBash() {
406
    Path settingsPath = getWindowsTerminalSettingsPath();
3✔
407
    if (settingsPath == null || !Files.exists(settingsPath)) {
7!
408
      LOG.warn("Windows Terminal settings file not found. Cannot configure Git Bash integration.");
×
409
      return;
×
410
    }
411

412
    try {
413
      Path bashPath = this.context.findBash();
4✔
414
      if (bashPath == null) {
2!
415
        LOG.warn("Git Bash not found. Cannot configure Windows Terminal integration.");
×
416
        return;
×
417
      }
418

419
      configureGitBashProfile(settingsPath, bashPath.toString());
5✔
420
      IdeLogLevel.SUCCESS.log(LOG, "Git Bash has been configured in Windows Terminal.");
4✔
421
    } catch (Exception e) {
×
422
      LOG.warn("Failed to configure Git Bash in Windows Terminal: {}", e.getMessage());
×
423
    }
1✔
424
  }
1✔
425

426
  /**
427
   * Gets the Windows Terminal settings file path.
428
   *
429
   * @return the {@link Path} to the Windows Terminal settings file, or {@code null} if not found.
430
   */
431
  protected Path getWindowsTerminalSettingsPath() {
432
    Path localAppData = this.context.getUserHome().resolve("AppData").resolve("Local");
8✔
433
    Path packagesPath = localAppData.resolve("Packages");
4✔
434

435
    // Try the new Windows Terminal package first
436
    Path newTerminalPath = packagesPath.resolve("Microsoft.WindowsTerminal_8wekyb3d8bbwe")
4✔
437
        .resolve("LocalState").resolve("settings.json");
4✔
438
    if (Files.exists(newTerminalPath)) {
5!
439
      return newTerminalPath;
2✔
440
    }
441

442
    // Try the old Windows Terminal Preview package
443
    Path previewPath = packagesPath.resolve("Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe")
×
444
        .resolve("LocalState").resolve("settings.json");
×
445
    if (Files.exists(previewPath)) {
×
446
      return previewPath;
×
447
    }
448

449
    return null;
×
450
  }
451

452
  /**
453
   * Configures Git Bash profile in Windows Terminal settings.
454
   *
455
   * @param settingsPath the {@link Path} to the Windows Terminal settings file.
456
   * @param bashPath the path to the Git Bash executable.
457
   */
458
  private void configureGitBashProfile(Path settingsPath, String bashPath) throws Exception {
459

460
    ObjectMapper mapper = new ObjectMapper();
4✔
461
    ObjectNode root;
462
    try (Reader reader = Files.newBufferedReader(settingsPath)) {
3✔
463
      JsonNode rootNode = mapper.readTree(reader);
4✔
464
      root = (ObjectNode) rootNode;
3✔
465
    }
466

467
    // Get or create profiles object
468
    ObjectNode profiles = (ObjectNode) root.get("profiles");
5✔
469
    if (profiles == null) {
2!
470
      profiles = mapper.createObjectNode();
×
471
      root.set("profiles", profiles);
×
472
    }
473

474
    // Get or create list array of profiles object
475
    JsonNode profilesList = profiles.get("list");
4✔
476
    if (profilesList == null || !profilesList.isArray()) {
5!
477
      profilesList = mapper.createArrayNode();
×
478
      profiles.set("list", profilesList);
×
479
    }
480

481
    // Check if Git Bash profile already exists
482
    boolean gitBashProfileExists = false;
2✔
483
    for (JsonNode profile : profilesList) {
10✔
484
      if (profile.has("name") && profile.get("name").asText().equals("Git Bash")) {
11!
485
        gitBashProfileExists = true;
×
486
        break;
×
487
      }
488
    }
1✔
489

490
    // Add Git Bash profile if it doesn't exist
491
    if (gitBashProfileExists) {
2!
492
      LOG.info("Git Bash profile already exists in {}.", settingsPath);
×
493
    } else {
494
      ObjectNode gitBashProfile = mapper.createObjectNode();
3✔
495
      String newGuid = "{2ece5bfe-50ed-5f3a-ab87-5cd4baafed2b}";
2✔
496
      String iconPath = getGitBashIconPath(bashPath);
4✔
497
      Path startingDirectory = Objects.requireNonNullElse(this.context.getIdeRoot(), this.context.getUserHome());
9✔
498

499
      gitBashProfile.put("guid", newGuid);
5✔
500
      gitBashProfile.put("name", "Git Bash");
5✔
501
      gitBashProfile.put("commandline", bashPath);
5✔
502
      gitBashProfile.put("icon", iconPath);
5✔
503
      gitBashProfile.put("startingDirectory", startingDirectory.toString());
6✔
504

505
      ((ArrayNode) profilesList).add(gitBashProfile);
5✔
506

507
      // Set Git Bash as default profile
508
      root.put("defaultProfile", newGuid);
5✔
509

510
      // Write back to file
511
      try (Writer writer = Files.newBufferedWriter(settingsPath)) {
5✔
512
        mapper.writerWithDefaultPrettyPrinter().writeValue(writer, root);
5✔
513
      }
514
    }
515
  }
1✔
516

517
  private String getGitBashIconPath(String bashPathString) {
518
    Path bashPath = Path.of(bashPathString);
5✔
519
    // "C:\\Program Files\\Git\\bin\\bash.exe"
520
    // "C:\\Program Files\\Git\\mingw64\\share\\git\\git-for-windows.ico"
521
    Path parent = bashPath.getParent();
3✔
522
    if (parent != null) {
2!
523
      Path iconPath = parent.resolve("mingw64/share/git/git-for-windows.ico");
4✔
524
      if (Files.exists(iconPath)) {
5!
525
        LOG.debug("Found git-bash icon at {}", iconPath);
×
526
        return iconPath.toString();
×
527
      }
528
      LOG.debug("Git Bash icon not found at {}. Using default icon.", iconPath);
4✔
529
    }
530
    return "ms-appx:///ProfileIcons/{0caa0dad-35be-5f56-a8ff-afceeeaa6101}.png";
2✔
531
  }
532

533
  static String removeObsoleteEntryFromWindowsPath(String userPath) {
534
    return removeEntryFromWindowsPath(userPath, IDE_BIN);
4✔
535
  }
536

537
  static String removeEntryFromWindowsPath(String userPath, String suffix) {
538
    int len = userPath.length();
3✔
539
    int start = 0;
2✔
540
    while ((start >= 0) && (start < len)) {
5!
541
      int end = userPath.indexOf(';', start);
5✔
542
      if (end < 0) {
2✔
543
        end = len;
2✔
544
      }
545
      String entry = userPath.substring(start, end);
5✔
546
      if (entry.endsWith(suffix)) {
4✔
547
        String prefix = "";
2✔
548
        int offset = 1;
2✔
549
        if (start > 0) {
2✔
550
          prefix = userPath.substring(0, start - 1);
7✔
551
          offset = 0;
2✔
552
        }
553
        if (end == len) {
3✔
554
          return prefix;
2✔
555
        } else {
556
          return removeEntryFromWindowsPath(prefix + userPath.substring(end + offset), suffix);
10✔
557
        }
558
      }
559
      start = end + 1;
4✔
560
    }
1✔
561
    return userPath;
2✔
562
  }
563

564
  /**
565
   * Adds ourselves to the shell RC (run-commands) configuration file.
566
   *
567
   * @param filename the name of the RC file.
568
   * @param ideRoot the IDE_ROOT {@link Path}.
569
   */
570
  private void addToShellRc(String filename, Path ideRoot, String extraLine) {
571

572
    modifyShellRc(filename, ideRoot, true, extraLine);
6✔
573
  }
1✔
574

575
  private void removeFromShellRc(String filename, Path ideRoot) {
576

577
    modifyShellRc(filename, ideRoot, false, null);
6✔
578
  }
1✔
579

580
  /**
581
   * Adds ourselves to the shell RC (run-commands) configuration file.
582
   *
583
   * @param filename the name of the RC file.
584
   * @param ideRoot the IDE_ROOT {@link Path}.
585
   */
586
  private void modifyShellRc(String filename, Path ideRoot, boolean add, String extraLine) {
587

588
    if (add) {
2✔
589
      LOG.info("Configuring IDEasy in {}", filename);
5✔
590
    } else {
591
      LOG.info("Removing IDEasy from {}", filename);
4✔
592
    }
593
    Path rcFile = this.context.getUserHome().resolve(filename);
6✔
594
    FileAccess fileAccess = this.context.getFileAccess();
4✔
595
    List<String> lines = fileAccess.readFileLines(rcFile);
4✔
596
    if (lines == null) {
2✔
597
      if (!add) {
2!
598
        return;
×
599
      }
600
      lines = new ArrayList<>();
5✔
601
    } else {
602
      // since it is unspecified if the returned List may be immutable we want to get sure
603
      lines = new ArrayList<>(lines);
5✔
604
    }
605
    Iterator<String> iterator = lines.iterator();
3✔
606
    int removeCount = 0;
2✔
607
    while (iterator.hasNext()) {
3✔
608
      String line = iterator.next();
4✔
609
      line = line.trim();
3✔
610
      if (isObsoleteRcLine(line)) {
3✔
611
        LOG.info("Removing obsolete line from {}: {}", filename, line);
5✔
612
        iterator.remove();
2✔
613
        removeCount++;
2✔
614
      } else if (line.equals(extraLine)) {
4✔
615
        extraLine = null;
2✔
616
      }
617
    }
1✔
618
    if (add) {
2✔
619
      if (extraLine != null) {
2!
620
        lines.add(extraLine);
×
621
      }
622
      if (!this.context.getSystemInfo().isWindows()) {
5✔
623
        lines.add("export IDE_ROOT=\"" + WindowsPathSyntax.MSYS.format(ideRoot) + "\"");
7✔
624
      }
625
      lines.add(BASH_CODE_SOURCE_FUNCTIONS);
4✔
626
    }
627
    fileAccess.writeFileLines(lines, rcFile);
4✔
628
    LOG.debug("Successfully updated {}", filename);
4✔
629
  }
1✔
630

631
  private static boolean isObsoleteRcLine(String line) {
632
    if (line.startsWith("alias ide=")) {
4!
633
      return true;
×
634
    } else if (line.startsWith("export IDE_ROOT=")) {
4!
635
      return true;
×
636
    } else if (line.equals("ide")) {
4✔
637
      return true;
2✔
638
    } else if (line.equals("ide init")) {
4✔
639
      return true;
2✔
640
    } else if (line.startsWith("source \"$IDE_ROOT/_ide/")) {
4✔
641
      return true;
2✔
642
    }
643
    return false;
2✔
644
  }
645

646
  private boolean addInstallationArtifact(Path cwd, String artifactName, boolean required, List<Path> installationArtifacts) {
647

648
    Path artifactPath = cwd.resolve(artifactName);
4✔
649
    if (Files.exists(artifactPath)) {
5!
650
      installationArtifacts.add(artifactPath);
5✔
651
    } else if (required) {
×
652
      LOG.error("Missing required file {}", artifactName);
×
653
      return false;
×
654
    }
655
    return true;
2✔
656
  }
657

658
  private Path determineIdeRoot(Path cwd) {
659
    Path ideRoot = this.context.getIdeRoot();
4✔
660
    if (ideRoot == null) {
2✔
661
      Path home = this.context.getUserHome();
4✔
662
      Path installRoot = home;
2✔
663
      if (this.context.getSystemInfo().isWindows()) {
5✔
664
        if (!cwd.startsWith(home)) {
4!
665
          installRoot = cwd.getRoot();
×
666
        }
667
      }
668
      ideRoot = installRoot.resolve(IdeContext.FOLDER_PROJECTS);
4✔
669
    } else {
1✔
670
      assert (Files.isDirectory(ideRoot)) : "IDE_ROOT directory does not exist!";
6!
671
    }
672
    return ideRoot;
2✔
673
  }
674

675
  /**
676
   * Uninstalls IDEasy entirely from the system.
677
   */
678
  public void uninstallIdeasy() {
679

680
    Path ideRoot = this.context.getIdeRoot();
4✔
681
    removeFromShellRc(BASHRC, ideRoot);
4✔
682
    removeFromShellRc(ZSHRC, ideRoot);
4✔
683
    Path idePath = this.context.getIdePath();
4✔
684
    uninstallIdeasyWindowsEnv(ideRoot);
3✔
685
    uninstallIdeasyIdePath(idePath);
3✔
686
    deleteDownloadCache();
2✔
687
    IdeLogLevel.SUCCESS.log(LOG, "IDEasy has been uninstalled from your system.");
4✔
688
    IdeLogLevel.INTERACTION.log(LOG, "ATTENTION:\n"
10✔
689
        + "In order to prevent data-loss, we do not delete your projects and git repositories!\n"
690
        + "To entirely get rid of IDEasy, also check your IDE_ROOT folder at:\n"
691
        + "{}", ideRoot);
692
  }
1✔
693

694
  private void deleteDownloadCache() {
695
    Path downloadPath = this.context.getDownloadPath();
4✔
696
    LOG.info("Deleting download cache from {}", downloadPath);
4✔
697
    tryDeleteDownloadCache(downloadPath);
3✔
698
    // older versions kept the cache under ~/Downloads/ide - clean that up too if it's still around
699
    Path legacy = this.context.getUserHome().resolve("Downloads/ide");
6✔
700
    if (!legacy.equals(downloadPath) && Files.exists(legacy)) {
4!
701
      LOG.info("Deleting legacy download cache from {}", legacy);
×
702
      tryDeleteDownloadCache(legacy);
×
703
    }
704
  }
1✔
705

706
  private void tryDeleteDownloadCache(Path path) {
707
    try {
708
      this.context.getFileAccess().delete(path);
5✔
709
    } catch (IllegalStateException e) {
×
710
      // best effort - on macOS ~/Downloads can deny access (EPERM), don't fail the whole uninstall over it
711
      String cause = (e.getCause() != null) ? e.getCause().getMessage() : e.getMessage();
×
712
      LOG.warn("Could not delete download cache at {} ({}). The folder will be left in place; you can remove it manually via Finder.", path, cause);
×
713
    }
1✔
714
  }
1✔
715

716
  private void uninstallIdeasyIdePath(Path idePath) {
717
    if (this.context.getSystemInfo().isWindows()) {
5✔
718
      Path bash = this.context.findBash();
4✔
719
      if (bash == null) {
2!
720
        LOG.warn("Could not find bash for asynchronous deletion of {}. Falling back to direct deletion.", idePath);
×
721
        this.context.getFileAccess().delete(idePath);
×
722
        return;
×
723
      }
724
      this.context.newProcess().executable(bash).addArgs("-c",
17✔
725
          "sleep 10 && rm -rf \"" + WindowsPathSyntax.MSYS.format(idePath) + "\"").run(ProcessMode.BACKGROUND);
5✔
726
      IdeLogLevel.INTERACTION.log(LOG,
10✔
727
          "To prevent windows file locking errors, we perform an asynchronous deletion of {} in background now.\n"
728
              + "Please close all terminals and wait a minute for the deletion to complete before running other commands.", idePath);
729
    } else {
1✔
730
      LOG.info("Finally deleting {}", idePath);
4✔
731
      this.context.getFileAccess().delete(idePath);
5✔
732
    }
733
  }
1✔
734

735
  private void uninstallIdeasyWindowsEnv(Path ideRoot) {
736
    if (!this.context.getSystemInfo().isWindows()) {
5✔
737
      return;
1✔
738
    }
739
    WindowsHelper helper = WindowsHelper.get(this.context);
4✔
740
    helper.removeUserEnvironmentValue(IdeVariables.IDE_ROOT.getName());
4✔
741
    String userPath = helper.getUserEnvironmentValue(IdeVariables.PATH.getName());
5✔
742
    if (userPath == null) {
2!
743
      LOG.error("Could not read user PATH from registry!");
×
744
    } else {
745
      LOG.info("Found user PATH={}", userPath);
4✔
746
      String newUserPath = userPath;
2✔
747
      if (!userPath.isEmpty()) {
3!
748
        SimpleSystemPath path = SimpleSystemPath.of(userPath, ';');
4✔
749
        path.removeEntries(s -> s.endsWith(IDE_BIN) || s.endsWith(IDE_INSTALLATION_BIN));
15!
750
        newUserPath = path.toString();
3✔
751
      }
752
      if (newUserPath.equals(userPath)) {
4!
753
        LOG.error("Could not find IDEasy in PATH:\n{}", userPath);
×
754
      } else {
755
        helper.setUserEnvironmentValue(IdeVariables.PATH.getName(), newUserPath);
5✔
756
      }
757
    }
758
  }
1✔
759
}
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