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

devonfw / IDEasy / 25682255870

11 May 2026 04:13PM UTC coverage: 70.743% (+0.03%) from 70.714%
25682255870

push

github

web-flow
#1892: fixed gui-launcher pom.xml not being included in native images on nightly/release builds. (#1894)

Co-authored-by: Jörg Hohwiller <hohwille@users.noreply.github.com>

4430 of 6910 branches covered (64.11%)

Branch coverage included in aggregate %.

11393 of 15457 relevant lines covered (73.71%)

3.12 hits per line

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

72.78
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.Set;
12

13
import org.slf4j.Logger;
14
import org.slf4j.LoggerFactory;
15

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

40
/**
41
 * {@link MvnBasedLocalToolCommandlet} for IDEasy (ide-cli).
42
 */
43
public class IdeasyCommandlet extends MvnBasedLocalToolCommandlet {
44

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

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

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

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

59

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

72
  private final UpgradeMode mode;
73

74
  /**
75
   * The constructor.
76
   *
77
   * @param context the {@link IdeContext}.
78
   */
79
  public IdeasyCommandlet(IdeContext context) {
80
    this(context, UpgradeMode.STABLE);
4✔
81
  }
1✔
82

83
  /**
84
   * The constructor.
85
   *
86
   * @param context the {@link IdeContext}.
87
   * @param mode the {@link UpgradeMode}.
88
   */
89
  public IdeasyCommandlet(IdeContext context, UpgradeMode mode) {
90

91
    super(context, TOOL_NAME, ARTIFACT, Set.of(Tag.PRODUCTIVITY, Tag.IDE));
8✔
92
    this.mode = mode;
3✔
93
  }
1✔
94

95
  @Override
96
  public VersionIdentifier getInstalledVersion() {
97

98
    return IdeVersion.getVersionIdentifier();
2✔
99
  }
100

101
  @Override
102
  public String getInstalledEdition() {
103

104
    return this.tool;
3✔
105
  }
106

107
  @Override
108
  public String getConfiguredEdition() {
109

110
    return this.tool;
3✔
111
  }
112

113
  @Override
114
  public VersionIdentifier getConfiguredVersion() {
115

116
    UpgradeMode upgradeMode = this.mode;
3✔
117
    if (upgradeMode == null) {
2!
118
      if (IdeVersion.isSnapshot()) {
2!
119
        upgradeMode = UpgradeMode.SNAPSHOT;
3✔
120
      } else {
121
        if (IdeVersion.getVersionIdentifier().getDevelopmentPhase().isStable()) {
×
122
          upgradeMode = UpgradeMode.STABLE;
×
123
        } else {
124
          upgradeMode = UpgradeMode.UNSTABLE;
×
125
        }
126
      }
127
    }
128
    return upgradeMode.getVersion();
3✔
129
  }
130

131
  @Override
132
  public Path getToolPath() {
133

134
    return this.context.getIdeInstallationPath();
4✔
135
  }
136

137
  @Override
138
  protected ToolInstallation doInstall(ToolInstallRequest request) {
139

140
    this.context.requireOnline("upgrade of IDEasy", true);
5✔
141

142
    if (IdeVersion.isUndefined() && !this.context.isForceMode()) {
6!
143
      VersionIdentifier version = IdeVersion.getVersionIdentifier();
2✔
144
      LOG.warn("You are using IDEasy version {} which indicates local development - skipping upgrade.", version);
4✔
145
      return toolAlreadyInstalled(request);
4✔
146
    }
147
    return super.doInstall(request);
×
148
  }
149

150
  /**
151
   * @return the latest released {@link VersionIdentifier version} of IDEasy.
152
   */
153
  public VersionIdentifier getLatestVersion() {
154

155
    if (!this.context.isForceMode()) {
4!
156
      VersionIdentifier currentVersion = IdeVersion.getVersionIdentifier();
2✔
157
      if (IdeVersion.isUndefined()) {
2!
158
        return currentVersion;
2✔
159
      }
160
    }
161
    VersionIdentifier configuredVersion = getConfiguredVersion();
×
162
    return getToolRepository().resolveVersion(this.tool, getConfiguredEdition(), configuredVersion, this);
×
163
  }
164

165
  /**
166
   * Checks if an update is available and logs according information.
167
   *
168
   * @return {@code true} if an update is available, {@code false} otherwise.
169
   */
170
  public boolean checkIfUpdateIsAvailable() {
171
    VersionIdentifier installedVersion = getInstalledVersion();
3✔
172
    IdeLogLevel.SUCCESS.log(LOG, "Your version of IDEasy is {}.", installedVersion);
10✔
173
    if (IdeVersion.isSnapshot()) {
2!
174
      LOG.warn("You are using a SNAPSHOT version of IDEasy. For stability consider switching to a stable release via 'ide upgrade --mode=stable'");
3✔
175
    }
176
    if (this.context.isOffline()) {
4✔
177
      LOG.warn("Skipping check for newer version of IDEasy because you are offline.");
3✔
178
      return false;
2✔
179
    }
180
    VersionIdentifier latestVersion = getLatestVersion();
3✔
181
    if (installedVersion.equals(latestVersion)) {
4!
182
      IdeLogLevel.SUCCESS.log(LOG, "Your are using the latest version of IDEasy and no update is available.");
4✔
183
      return false;
2✔
184
    } else {
185
      IdeLogLevel.INTERACTION.log(LOG,
×
186
          "Your version of IDEasy is {} but version {} is available. Please run the following command to upgrade to the latest version:\n"
187
              + "ide upgrade", installedVersion, latestVersion);
188
      return true;
×
189
    }
190
  }
191

192
  /**
193
   * Initial installation of IDEasy.
194
   *
195
   * @param cwd the {@link Path} to the current working directory.
196
   * @see com.devonfw.tools.ide.commandlet.InstallCommandlet
197
   */
198
  public void installIdeasy(Path cwd) {
199
    Path ideRoot = determineIdeRoot(cwd);
4✔
200
    Path idePath = ideRoot.resolve(IdeContext.FOLDER_UNDERSCORE_IDE);
4✔
201
    Path installationPath = idePath.resolve(IdeContext.FOLDER_INSTALLATION);
4✔
202
    Path ideasySoftwarePath = idePath.resolve(IdeContext.FOLDER_SOFTWARE).resolve(MvnRepository.ID).resolve(IdeasyCommandlet.TOOL_NAME)
8✔
203
        .resolve(IdeasyCommandlet.TOOL_NAME);
2✔
204
    Path ideasyVersionPath = ideasySoftwarePath.resolve(IdeVersion.getVersionString());
4✔
205
    FileAccess fileAccess = this.context.getFileAccess();
4✔
206
    if (Files.isDirectory(ideasyVersionPath)) {
5✔
207
      LOG.error("IDEasy is already installed at {} - if your installation is broken, delete it manually and rerun setup!", ideasyVersionPath);
5✔
208
    } else {
209
      List<Path> installationArtifacts = new ArrayList<>();
4✔
210
      for (Map.Entry<String, Boolean> artifactEntry : REQUIRED_INSTALLATION_ARTIFACTS.entrySet()) {
11✔
211
        String artifactName = artifactEntry.getKey();
4✔
212
        boolean required = artifactEntry.getValue();
5✔
213
        boolean success = addInstallationArtifact(cwd, artifactName, required, installationArtifacts);
7✔
214
        if (!success) {
2!
215
          throw new CliException("IDEasy release is inconsistent at %s [artifact=%s]".formatted(cwd, artifactName));
×
216
        }
217
      }
1✔
218

219
      fileAccess.mkdirs(ideasyVersionPath);
3✔
220
      for (Path installationArtifact : installationArtifacts) {
10✔
221
        fileAccess.copy(installationArtifact, ideasyVersionPath);
4✔
222
      }
1✔
223
      this.context.writeVersionFile(IdeVersion.getVersionIdentifier(), ideasyVersionPath);
5✔
224
    }
225
    fileAccess.symlink(ideasyVersionPath, installationPath);
4✔
226
    addToShellRc(BASHRC, ideRoot, null);
5✔
227
    addToShellRc(ZSHRC, ideRoot, "autoload -U +X bashcompinit && bashcompinit");
5✔
228
    installIdeasyWindowsEnv(ideRoot, installationPath);
4✔
229
    IdeLogLevel.SUCCESS.log(LOG, "IDEasy has been installed successfully on your system.");
4✔
230
    LOG.warn("IDEasy has been setup for new shells but it cannot work in your current shell(s).\n"
3✔
231
        + "To use it here, run 'source ~/.bashrc' (or your shell config). Otherwise, open a new terminal or reboot.");
232
  }
1✔
233

234
  private void installIdeasyWindowsEnv(Path ideRoot, Path installationPath) {
235
    if (!this.context.getSystemInfo().isWindows()) {
5✔
236
      return;
1✔
237
    }
238
    WindowsHelper helper = WindowsHelper.get(this.context);
4✔
239
    helper.setUserEnvironmentValue(IdeVariables.IDE_ROOT.getName(), ideRoot.toString());
6✔
240
    String userPath = helper.getUserEnvironmentValue(IdeVariables.PATH.getName());
5✔
241
    if (userPath == null) {
2!
242
      LOG.error("Could not read user PATH from registry!");
×
243
    } else {
244
      LOG.info("Found user PATH={}", userPath);
4✔
245
      Path ideasyBinPath = installationPath.resolve("bin");
4✔
246
      SimpleSystemPath path = SimpleSystemPath.of(userPath, ';');
4✔
247
      if (path.getEntries().isEmpty()) {
4!
248
        LOG.warn("ATTENTION:\n"
×
249
            + "Your user specific PATH variable seems to be empty.\n"
250
            + "You can double check this by pressing [Windows][r] and launch the program SystemPropertiesAdvanced.\n"
251
            + "Then click on 'Environment variables' and check if 'PATH' is set in the 'user variables' from the upper list.\n"
252
            + "In case 'PATH' is defined there non-empty and you get this message, please abort and give us feedback:\n"
253
            + "https://github.com/devonfw/IDEasy/issues\n"
254
            + "Otherwise all is correct and you can continue.");
255
        this.context.askToContinue("Are you sure you want to override your PATH?");
×
256
      } else {
257
        path.removeEntries(s -> s.endsWith(IDE_INSTALLATION_BIN));
7✔
258
      }
259
      path.getEntries().add(ideasyBinPath.toString());
6✔
260
      helper.setUserEnvironmentValue(IdeVariables.PATH.getName(), path.toString());
6✔
261
      setGitLongpaths();
2✔
262
    }
263
  }
1✔
264

265
  private void setGitLongpaths() {
266
    this.context.getGitContext().findGitRequired();
5✔
267
    Path configPath = this.context.getUserHome().resolve(".gitconfig");
6✔
268
    FileAccess fileAccess = this.context.getFileAccess();
4✔
269
    IniFile iniFile = fileAccess.readIniFile(configPath);
4✔
270
    IniSection coreSection = iniFile.getOrCreateSection("core");
4✔
271
    coreSection.setProperty("longpaths", "true");
4✔
272
    fileAccess.writeIniFile(iniFile, configPath);
4✔
273
  }
1✔
274

275
  /**
276
   * Sets up Windows Terminal with Git Bash integration.
277
   */
278
  public void setupWindowsTerminal() {
279
    if (!this.context.getSystemInfo().isWindows()) {
×
280
      return;
×
281
    }
282
    if (!isWindowsTerminalInstalled()) {
×
283
      try {
284
        installWindowsTerminal();
×
285
      } catch (Exception e) {
×
286
        LOG.error("Failed to install Windows Terminal!", e);
×
287
      }
×
288
    }
289
    configureWindowsTerminalGitBash();
×
290
  }
×
291

292
  /**
293
   * Checks if Windows Terminal is installed.
294
   *
295
   * @return {@code true} if Windows Terminal is installed, {@code false} otherwise.
296
   */
297
  private boolean isWindowsTerminalInstalled() {
298
    try {
299
      ProcessResult result = this.context.newProcess()
×
300
          .executable("powershell")
×
301
          .addArgs("-Command", "Get-AppxPackage -Name Microsoft.WindowsTerminal")
×
302
          .run(ProcessMode.DEFAULT_CAPTURE);
×
303
      return result.isSuccessful() && !result.getOut().isEmpty();
×
304
    } catch (Exception e) {
×
305
      LOG.debug("Failed to check Windows Terminal installation.", e);
×
306
      return false;
×
307
    }
308
  }
309

310
  /**
311
   * Installs Windows Terminal using winget.
312
   */
313
  private void installWindowsTerminal() {
314
    try {
315
      LOG.info("Installing Windows Terminal...");
×
316
      ProcessResult result = this.context.newProcess()
×
317
          .executable("winget")
×
318
          .addArgs("install", "Microsoft.WindowsTerminal")
×
319
          .run(ProcessMode.DEFAULT);
×
320
      if (result.isSuccessful()) {
×
321
        IdeLogLevel.SUCCESS.log(LOG, "Windows Terminal has been installed successfully.");
×
322
      } else {
323
        LOG.warn("Failed to install Windows Terminal. Please install it manually from Microsoft Store.");
×
324
      }
325
    } catch (Exception e) {
×
326
      LOG.warn("Failed to install Windows Terminal: {}. Please install it manually from Microsoft Store.", e.toString());
×
327
    }
×
328
  }
×
329

330
  /**
331
   * Configures Git Bash integration in Windows Terminal.
332
   */
333
  protected void configureWindowsTerminalGitBash() {
334
    Path settingsPath = getWindowsTerminalSettingsPath();
3✔
335
    if (settingsPath == null || !Files.exists(settingsPath)) {
7!
336
      LOG.warn("Windows Terminal settings file not found. Cannot configure Git Bash integration.");
×
337
      return;
×
338
    }
339

340
    try {
341
      Path bashPath = this.context.findBash();
4✔
342
      if (bashPath == null) {
2!
343
        LOG.warn("Git Bash not found. Cannot configure Windows Terminal integration.");
×
344
        return;
×
345
      }
346

347
      configureGitBashProfile(settingsPath, bashPath.toString());
5✔
348
      IdeLogLevel.SUCCESS.log(LOG, "Git Bash has been configured in Windows Terminal.");
4✔
349
    } catch (Exception e) {
×
350
      LOG.warn("Failed to configure Git Bash in Windows Terminal: {}", e.getMessage());
×
351
    }
1✔
352
  }
1✔
353

354
  /**
355
   * Gets the Windows Terminal settings file path.
356
   *
357
   * @return the {@link Path} to the Windows Terminal settings file, or {@code null} if not found.
358
   */
359
  protected Path getWindowsTerminalSettingsPath() {
360
    Path localAppData = this.context.getUserHome().resolve("AppData").resolve("Local");
8✔
361
    Path packagesPath = localAppData.resolve("Packages");
4✔
362

363
    // Try the new Windows Terminal package first
364
    Path newTerminalPath = packagesPath.resolve("Microsoft.WindowsTerminal_8wekyb3d8bbwe")
4✔
365
        .resolve("LocalState").resolve("settings.json");
4✔
366
    if (Files.exists(newTerminalPath)) {
5!
367
      return newTerminalPath;
2✔
368
    }
369

370
    // Try the old Windows Terminal Preview package
371
    Path previewPath = packagesPath.resolve("Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe")
×
372
        .resolve("LocalState").resolve("settings.json");
×
373
    if (Files.exists(previewPath)) {
×
374
      return previewPath;
×
375
    }
376

377
    return null;
×
378
  }
379

380
  /**
381
   * Configures Git Bash profile in Windows Terminal settings.
382
   *
383
   * @param settingsPath the {@link Path} to the Windows Terminal settings file.
384
   * @param bashPath the path to the Git Bash executable.
385
   */
386
  private void configureGitBashProfile(Path settingsPath, String bashPath) throws Exception {
387

388
    ObjectMapper mapper = new ObjectMapper();
4✔
389
    ObjectNode root;
390
    try (Reader reader = Files.newBufferedReader(settingsPath)) {
3✔
391
      JsonNode rootNode = mapper.readTree(reader);
4✔
392
      root = (ObjectNode) rootNode;
3✔
393
    }
394

395
    // Get or create profiles object
396
    ObjectNode profiles = (ObjectNode) root.get("profiles");
5✔
397
    if (profiles == null) {
2!
398
      profiles = mapper.createObjectNode();
×
399
      root.set("profiles", profiles);
×
400
    }
401

402
    // Get or create list array of profiles object
403
    JsonNode profilesList = profiles.get("list");
4✔
404
    if (profilesList == null || !profilesList.isArray()) {
5!
405
      profilesList = mapper.createArrayNode();
×
406
      profiles.set("list", profilesList);
×
407
    }
408

409
    // Check if Git Bash profile already exists
410
    boolean gitBashProfileExists = false;
2✔
411
    for (JsonNode profile : profilesList) {
10✔
412
      if (profile.has("name") && profile.get("name").asText().equals("Git Bash")) {
11!
413
        gitBashProfileExists = true;
×
414
        break;
×
415
      }
416
    }
1✔
417

418
    // Add Git Bash profile if it doesn't exist
419
    if (gitBashProfileExists) {
2!
420
      LOG.info("Git Bash profile already exists in {}.", settingsPath);
×
421
    } else {
422
      ObjectNode gitBashProfile = mapper.createObjectNode();
3✔
423
      String newGuid = "{2ece5bfe-50ed-5f3a-ab87-5cd4baafed2b}";
2✔
424
      String iconPath = getGitBashIconPath(bashPath);
4✔
425
      String startingDirectory = this.context.getIdeRoot().toString();
5✔
426

427
      gitBashProfile.put("guid", newGuid);
5✔
428
      gitBashProfile.put("name", "Git Bash");
5✔
429
      gitBashProfile.put("commandline", bashPath);
5✔
430
      gitBashProfile.put("icon", iconPath);
5✔
431
      gitBashProfile.put("startingDirectory", startingDirectory);
5✔
432

433
      ((ArrayNode) profilesList).add(gitBashProfile);
5✔
434

435
      // Set Git Bash as default profile
436
      root.put("defaultProfile", newGuid);
5✔
437

438
      // Write back to file
439
      try (Writer writer = Files.newBufferedWriter(settingsPath)) {
5✔
440
        mapper.writerWithDefaultPrettyPrinter().writeValue(writer, root);
5✔
441
      }
442
    }
443
  }
1✔
444

445
  private String getGitBashIconPath(String bashPathString) {
446
    Path bashPath = Path.of(bashPathString);
5✔
447
    // "C:\\Program Files\\Git\\bin\\bash.exe"
448
    // "C:\\Program Files\\Git\\mingw64\\share\\git\\git-for-windows.ico"
449
    Path parent = bashPath.getParent();
3✔
450
    if (parent != null) {
2!
451
      Path iconPath = parent.resolve("mingw64/share/git/git-for-windows.ico");
4✔
452
      if (Files.exists(iconPath)) {
5!
453
        LOG.debug("Found git-bash icon at {}", iconPath);
×
454
        return iconPath.toString();
×
455
      }
456
      LOG.debug("Git Bash icon not found at {}. Using default icon.", iconPath);
4✔
457
    }
458
    return "ms-appx:///ProfileIcons/{0caa0dad-35be-5f56-a8ff-afceeeaa6101}.png";
2✔
459
  }
460

461
  static String removeObsoleteEntryFromWindowsPath(String userPath) {
462
    return removeEntryFromWindowsPath(userPath, IDE_BIN);
4✔
463
  }
464

465
  static String removeEntryFromWindowsPath(String userPath, String suffix) {
466
    int len = userPath.length();
3✔
467
    int start = 0;
2✔
468
    while ((start >= 0) && (start < len)) {
5!
469
      int end = userPath.indexOf(';', start);
5✔
470
      if (end < 0) {
2✔
471
        end = len;
2✔
472
      }
473
      String entry = userPath.substring(start, end);
5✔
474
      if (entry.endsWith(suffix)) {
4✔
475
        String prefix = "";
2✔
476
        int offset = 1;
2✔
477
        if (start > 0) {
2✔
478
          prefix = userPath.substring(0, start - 1);
7✔
479
          offset = 0;
2✔
480
        }
481
        if (end == len) {
3✔
482
          return prefix;
2✔
483
        } else {
484
          return removeEntryFromWindowsPath(prefix + userPath.substring(end + offset), suffix);
10✔
485
        }
486
      }
487
      start = end + 1;
4✔
488
    }
1✔
489
    return userPath;
2✔
490
  }
491

492
  /**
493
   * Adds ourselves to the shell RC (run-commands) configuration file.
494
   *
495
   * @param filename the name of the RC file.
496
   * @param ideRoot the IDE_ROOT {@link Path}.
497
   */
498
  private void addToShellRc(String filename, Path ideRoot, String extraLine) {
499

500
    modifyShellRc(filename, ideRoot, true, extraLine);
6✔
501
  }
1✔
502

503
  private void removeFromShellRc(String filename, Path ideRoot) {
504

505
    modifyShellRc(filename, ideRoot, false, null);
6✔
506
  }
1✔
507

508
  /**
509
   * Adds ourselves to the shell RC (run-commands) configuration file.
510
   *
511
   * @param filename the name of the RC file.
512
   * @param ideRoot the IDE_ROOT {@link Path}.
513
   */
514
  private void modifyShellRc(String filename, Path ideRoot, boolean add, String extraLine) {
515

516
    if (add) {
2✔
517
      LOG.info("Configuring IDEasy in {}", filename);
5✔
518
    } else {
519
      LOG.info("Removing IDEasy from {}", filename);
4✔
520
    }
521
    Path rcFile = this.context.getUserHome().resolve(filename);
6✔
522
    FileAccess fileAccess = this.context.getFileAccess();
4✔
523
    List<String> lines = fileAccess.readFileLines(rcFile);
4✔
524
    if (lines == null) {
2✔
525
      if (!add) {
2!
526
        return;
×
527
      }
528
      lines = new ArrayList<>();
5✔
529
    } else {
530
      // since it is unspecified if the returned List may be immutable we want to get sure
531
      lines = new ArrayList<>(lines);
5✔
532
    }
533
    Iterator<String> iterator = lines.iterator();
3✔
534
    int removeCount = 0;
2✔
535
    while (iterator.hasNext()) {
3✔
536
      String line = iterator.next();
4✔
537
      line = line.trim();
3✔
538
      if (isObsoleteRcLine(line)) {
3✔
539
        LOG.info("Removing obsolete line from {}: {}", filename, line);
5✔
540
        iterator.remove();
2✔
541
        removeCount++;
2✔
542
      } else if (line.equals(extraLine)) {
4✔
543
        extraLine = null;
2✔
544
      }
545
    }
1✔
546
    if (add) {
2✔
547
      if (extraLine != null) {
2!
548
        lines.add(extraLine);
×
549
      }
550
      if (!this.context.getSystemInfo().isWindows()) {
5✔
551
        lines.add("export IDE_ROOT=\"" + WindowsPathSyntax.MSYS.format(ideRoot) + "\"");
7✔
552
      }
553
      lines.add(BASH_CODE_SOURCE_FUNCTIONS);
4✔
554
    }
555
    fileAccess.writeFileLines(lines, rcFile);
4✔
556
    LOG.debug("Successfully updated {}", filename);
4✔
557
  }
1✔
558

559
  private static boolean isObsoleteRcLine(String line) {
560
    if (line.startsWith("alias ide=")) {
4!
561
      return true;
×
562
    } else if (line.startsWith("export IDE_ROOT=")) {
4!
563
      return true;
×
564
    } else if (line.equals("ide")) {
4✔
565
      return true;
2✔
566
    } else if (line.equals("ide init")) {
4✔
567
      return true;
2✔
568
    } else if (line.startsWith("source \"$IDE_ROOT/_ide/")) {
4✔
569
      return true;
2✔
570
    }
571
    return false;
2✔
572
  }
573

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

576
    Path artifactPath = cwd.resolve(artifactName);
4✔
577
    if (Files.exists(artifactPath)) {
5!
578
      installationArtifacts.add(artifactPath);
5✔
579
    } else if (required) {
×
580
      LOG.error("Missing required file {}", artifactName);
×
581
      return false;
×
582
    }
583
    return true;
2✔
584
  }
585

586
  private Path determineIdeRoot(Path cwd) {
587
    Path ideRoot = this.context.getIdeRoot();
4✔
588
    if (ideRoot == null) {
2!
589
      Path home = this.context.getUserHome();
4✔
590
      Path installRoot = home;
2✔
591
      if (this.context.getSystemInfo().isWindows()) {
5✔
592
        if (!cwd.startsWith(home)) {
4!
593
          installRoot = cwd.getRoot();
×
594
        }
595
      }
596
      ideRoot = installRoot.resolve(IdeContext.FOLDER_PROJECTS);
4✔
597
    } else {
1✔
598
      assert (Files.isDirectory(ideRoot)) : "IDE_ROOT directory does not exist!";
×
599
    }
600
    return ideRoot;
2✔
601
  }
602

603
  /**
604
   * Uninstalls IDEasy entirely from the system.
605
   */
606
  public void uninstallIdeasy() {
607

608
    Path ideRoot = this.context.getIdeRoot();
4✔
609
    removeFromShellRc(BASHRC, ideRoot);
4✔
610
    removeFromShellRc(ZSHRC, ideRoot);
4✔
611
    Path idePath = this.context.getIdePath();
4✔
612
    uninstallIdeasyWindowsEnv(ideRoot);
3✔
613
    uninstallIdeasyIdePath(idePath);
3✔
614
    deleteDownloadCache();
2✔
615
    IdeLogLevel.SUCCESS.log(LOG, "IDEasy has been uninstalled from your system.");
4✔
616
    IdeLogLevel.INTERACTION.log(LOG, "ATTENTION:\n"
10✔
617
        + "In order to prevent data-loss, we do not delete your projects and git repositories!\n"
618
        + "To entirely get rid of IDEasy, also check your IDE_ROOT folder at:\n"
619
        + "{}", ideRoot);
620
  }
1✔
621

622
  private void deleteDownloadCache() {
623
    Path downloadPath = this.context.getDownloadPath();
4✔
624
    LOG.info("Deleting download cache from {}", downloadPath);
4✔
625
    this.context.getFileAccess().delete(downloadPath);
5✔
626
  }
1✔
627

628
  private void uninstallIdeasyIdePath(Path idePath) {
629
    if (this.context.getSystemInfo().isWindows()) {
5✔
630
      Path bash = this.context.findBash();
4✔
631
      if (bash == null) {
2!
632
        LOG.warn("Could not find bash for asynchronous deletion of {}. Falling back to direct deletion.", idePath);
×
633
        this.context.getFileAccess().delete(idePath);
×
634
        return;
×
635
      }
636
      this.context.newProcess().executable(bash).addArgs("-c",
17✔
637
          "sleep 10 && rm -rf \"" + WindowsPathSyntax.MSYS.format(idePath) + "\"").run(ProcessMode.BACKGROUND);
5✔
638
      IdeLogLevel.INTERACTION.log(LOG,
10✔
639
          "To prevent windows file locking errors, we perform an asynchronous deletion of {} in background now.\n"
640
              + "Please close all terminals and wait a minute for the deletion to complete before running other commands.", idePath);
641
    } else {
1✔
642
      LOG.info("Finally deleting {}", idePath);
4✔
643
      this.context.getFileAccess().delete(idePath);
5✔
644
    }
645
  }
1✔
646

647
  private void uninstallIdeasyWindowsEnv(Path ideRoot) {
648
    if (!this.context.getSystemInfo().isWindows()) {
5✔
649
      return;
1✔
650
    }
651
    WindowsHelper helper = WindowsHelper.get(this.context);
4✔
652
    helper.removeUserEnvironmentValue(IdeVariables.IDE_ROOT.getName());
4✔
653
    String userPath = helper.getUserEnvironmentValue(IdeVariables.PATH.getName());
5✔
654
    if (userPath == null) {
2!
655
      LOG.error("Could not read user PATH from registry!");
×
656
    } else {
657
      LOG.info("Found user PATH={}", userPath);
4✔
658
      String newUserPath = userPath;
2✔
659
      if (!userPath.isEmpty()) {
3!
660
        SimpleSystemPath path = SimpleSystemPath.of(userPath, ';');
4✔
661
        path.removeEntries(s -> s.endsWith(IDE_BIN) || s.endsWith(IDE_INSTALLATION_BIN));
15!
662
        newUserPath = path.toString();
3✔
663
      }
664
      if (newUserPath.equals(userPath)) {
4!
665
        LOG.error("Could not find IDEasy in PATH:\n{}", userPath);
×
666
      } else {
667
        helper.setUserEnvironmentValue(IdeVariables.PATH.getName(), newUserPath);
5✔
668
      }
669
    }
670
  }
1✔
671
}
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