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

devonfw / IDEasy / 15932029172

27 Jun 2025 05:13PM UTC coverage: 68.261% (+0.009%) from 68.252%
15932029172

Pull #1348

github

web-flow
Merge 4dc00b327 into 98d1ed4f3
Pull Request #1348: #1331: add parser for .ini files

3257 of 5184 branches covered (62.83%)

Branch coverage included in aggregate %.

8316 of 11770 relevant lines covered (70.65%)

3.13 hits per line

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

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

3
import java.nio.file.Files;
4
import java.nio.file.Path;
5
import java.util.ArrayList;
6
import java.util.Iterator;
7
import java.util.List;
8
import java.util.Set;
9

10
import com.devonfw.tools.ide.cli.CliException;
11
import com.devonfw.tools.ide.commandlet.UpgradeMode;
12
import com.devonfw.tools.ide.common.SimpleSystemPath;
13
import com.devonfw.tools.ide.common.Tag;
14
import com.devonfw.tools.ide.context.IdeContext;
15
import com.devonfw.tools.ide.git.GitContext;
16
import com.devonfw.tools.ide.git.GitContextImpl;
17
import com.devonfw.tools.ide.io.FileAccess;
18
import com.devonfw.tools.ide.io.FileAccessImpl;
19
import com.devonfw.tools.ide.io.IniFile;
20
import com.devonfw.tools.ide.io.IniFileImpl;
21
import com.devonfw.tools.ide.io.IniSection;
22
import com.devonfw.tools.ide.os.WindowsHelper;
23
import com.devonfw.tools.ide.os.WindowsPathSyntax;
24
import com.devonfw.tools.ide.process.ProcessMode;
25
import com.devonfw.tools.ide.tool.mvn.MvnArtifact;
26
import com.devonfw.tools.ide.tool.mvn.MvnBasedLocalToolCommandlet;
27
import com.devonfw.tools.ide.tool.repository.MavenRepository;
28
import com.devonfw.tools.ide.variable.IdeVariables;
29
import com.devonfw.tools.ide.version.IdeVersion;
30
import com.devonfw.tools.ide.version.VersionIdentifier;
31

32
/**
33
 * {@link MvnBasedLocalToolCommandlet} for IDEasy (ide-cli).
34
 */
35
public class IdeasyCommandlet extends MvnBasedLocalToolCommandlet {
36

37
  /** The {@link MvnArtifact} for IDEasy. */
38
  public static final MvnArtifact ARTIFACT = MvnArtifact.ofIdeasyCli("*!", "tar.gz", "${os}-${arch}");
6✔
39

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

42
  /** The {@link #getName() tool name}. */
43
  public static final String TOOL_NAME = "ideasy";
44
  public static final String BASHRC = ".bashrc";
45
  public static final String ZSHRC = ".zshrc";
46
  public static final String IDE_BIN = "\\_ide\\bin";
47
  public static final String IDE_INSTALLATION_BIN = "\\_ide\\installation\\bin";
48

49
  private final UpgradeMode mode;
50

51
  /**
52
   * The constructor.
53
   *
54
   * @param context the {@link IdeContext}.
55
   */
56
  public IdeasyCommandlet(IdeContext context) {
57
    this(context, UpgradeMode.STABLE);
4✔
58
  }
1✔
59

60
  /**
61
   * The constructor.
62
   *
63
   * @param context the {@link IdeContext}.
64
   * @param mode the {@link UpgradeMode}.
65
   */
66
  public IdeasyCommandlet(IdeContext context, UpgradeMode mode) {
67

68
    super(context, TOOL_NAME, ARTIFACT, Set.of(Tag.PRODUCTIVITY, Tag.IDE));
8✔
69
    this.mode = mode;
3✔
70
  }
1✔
71

72
  @Override
73
  public VersionIdentifier getInstalledVersion() {
74

75
    return IdeVersion.getVersionIdentifier();
2✔
76
  }
77

78
  @Override
79
  public String getConfiguredEdition() {
80

81
    return this.tool;
×
82
  }
83

84
  @Override
85
  public VersionIdentifier getConfiguredVersion() {
86

87
    UpgradeMode upgradeMode = this.mode;
×
88
    if (upgradeMode == null) {
×
89
      if (IdeVersion.getVersionString().contains("SNAPSHOT")) {
×
90
        upgradeMode = UpgradeMode.SNAPSHOT;
×
91
      } else {
92
        if (IdeVersion.getVersionIdentifier().getDevelopmentPhase().isStable()) {
×
93
          upgradeMode = UpgradeMode.STABLE;
×
94
        } else {
95
          upgradeMode = UpgradeMode.UNSTABLE;
×
96
        }
97
      }
98
    }
99
    return upgradeMode.getVersion();
×
100
  }
101

102
  @Override
103
  public Path getToolPath() {
104

105
    return this.context.getIdeInstallationPath();
×
106
  }
107

108
  @Override
109
  public boolean install(boolean silent) {
110
    
111
    this.context.requireOnline("upgrade of IDEasy", true);
5✔
112

113
    if (IdeVersion.isUndefined()) {
2!
114
      this.context.warning("You are using IDEasy version {} which indicates local development - skipping upgrade.", IdeVersion.getVersionString());
10✔
115
      return false;
2✔
116
    }
117
    return super.install(silent);
×
118
  }
119

120
  /**
121
   * @return the latest released {@link VersionIdentifier version} of IDEasy.
122
   */
123
  public VersionIdentifier getLatestVersion() {
124

125
    VersionIdentifier currentVersion = IdeVersion.getVersionIdentifier();
2✔
126
    if (IdeVersion.isUndefined()) {
2!
127
      return currentVersion;
2✔
128
    }
129
    VersionIdentifier configuredVersion = getConfiguredVersion();
×
130
    return getToolRepository().resolveVersion(this.tool, getConfiguredEdition(), configuredVersion, this);
×
131
  }
132

133
  /**
134
   * Checks if an update is available and logs according information.
135
   *
136
   * @return {@code true} if an update is available, {@code false} otherwise.
137
   */
138
  public boolean checkIfUpdateIsAvailable() {
139
    VersionIdentifier installedVersion = getInstalledVersion();
3✔
140
    if (!this.context.isOnline()) {
4✔
141
      this.context.success("Your version of IDEasy is {}.", installedVersion);
10✔
142
      this.context.warning("Skipping check for newer version of IDEasy due to lack of network connectivity.");
4✔
143
      return false;
2✔
144
    }
145
    VersionIdentifier latestVersion = getLatestVersion();
3✔
146
    if (installedVersion.equals(latestVersion)) {
4!
147
      this.context.success("Your version of IDEasy is {} which is the latest released version.", installedVersion);
10✔
148
      return false;
2✔
149
    } else {
150
      this.context.interaction("Your version of IDEasy is {} but version {} is available. Please run the following command to upgrade to the latest version:\n"
×
151
          + "ide upgrade", installedVersion, latestVersion);
152
      return true;
×
153
    }
154
  }
155

156
  /**
157
   * Initial installation of IDEasy.
158
   *
159
   * @param cwd the {@link Path} to the current working directory.
160
   * @see com.devonfw.tools.ide.commandlet.InstallCommandlet
161
   */
162
  public void installIdeasy(Path cwd) {
163
    Path ideRoot = determineIdeRoot(cwd);
4✔
164
    Path idePath = ideRoot.resolve(IdeContext.FOLDER_UNDERSCORE_IDE);
4✔
165
    Path installationPath = idePath.resolve(IdeContext.FOLDER_INSTALLATION);
4✔
166
    Path ideasySoftwarePath = idePath.resolve(IdeContext.FOLDER_SOFTWARE).resolve(MavenRepository.ID).resolve(IdeasyCommandlet.TOOL_NAME)
8✔
167
        .resolve(IdeasyCommandlet.TOOL_NAME);
2✔
168
    Path ideasyVersionPath = ideasySoftwarePath.resolve(IdeVersion.getVersionString());
4✔
169
    if (Files.isDirectory(ideasyVersionPath)) {
5!
170
      throw new CliException("IDEasy is already installed at " + ideasyVersionPath + " - if your installation is broken, delete it manually and rerun setup!");
×
171
    }
172
    FileAccess fileAccess = this.context.getFileAccess();
4✔
173
    List<Path> installationArtifacts = new ArrayList<>();
4✔
174
    boolean success = true;
2✔
175
    success &= addInstallationArtifact(cwd, "bin", true, installationArtifacts);
9✔
176
    success &= addInstallationArtifact(cwd, "functions", true, installationArtifacts);
9✔
177
    success &= addInstallationArtifact(cwd, "internal", true, installationArtifacts);
9✔
178
    success &= addInstallationArtifact(cwd, "system", true, installationArtifacts);
9✔
179
    success &= addInstallationArtifact(cwd, "IDEasy.pdf", true, installationArtifacts);
9✔
180
    success &= addInstallationArtifact(cwd, "setup", true, installationArtifacts);
9✔
181
    success &= addInstallationArtifact(cwd, "setup.bat", false, installationArtifacts);
9✔
182
    if (!success) {
2!
183
      throw new CliException("IDEasy release is inconsistent at " + cwd);
×
184
    }
185
    fileAccess.mkdirs(ideasyVersionPath);
3✔
186
    for (Path installationArtifact : installationArtifacts) {
10✔
187
      fileAccess.copy(installationArtifact, ideasyVersionPath);
4✔
188
    }
1✔
189
    this.context.writeVersionFile(IdeVersion.getVersionIdentifier(), ideasyVersionPath);
5✔
190
    fileAccess.symlink(ideasyVersionPath, installationPath);
4✔
191
    addToShellRc(BASHRC, ideRoot, null);
5✔
192
    addToShellRc(ZSHRC, ideRoot, "autoload -U +X bashcompinit && bashcompinit");
5✔
193
    installIdeasyWindowsEnv(ideRoot, installationPath);
4✔
194
    this.context.success("IDEasy has been installed successfully on your system.");
4✔
195
    this.context.warning("IDEasy has been setup for new shells but it cannot work in your current shell(s).\n"
4✔
196
        + "Reboot or open a new terminal to make it work.");
197
  }
1✔
198

199
  private void installIdeasyWindowsEnv(Path ideRoot, Path installationPath) {
200
    if (!this.context.getSystemInfo().isWindows()) {
5✔
201
      return;
1✔
202
    }
203
    WindowsHelper helper = WindowsHelper.get(this.context);
4✔
204
    helper.setUserEnvironmentValue(IdeVariables.IDE_ROOT.getName(), ideRoot.toString());
6✔
205
    String userPath = helper.getUserEnvironmentValue(IdeVariables.PATH.getName());
5✔
206
    if (userPath == null) {
2!
207
      this.context.error("Could not read user PATH from registry!");
×
208
    } else {
209
      this.context.info("Found user PATH={}", userPath);
10✔
210
      Path ideasyBinPath = installationPath.resolve("bin");
4✔
211
      SimpleSystemPath path = SimpleSystemPath.of(userPath, ';');
4✔
212
      if (path.getEntries().isEmpty()) {
4!
213
        this.context.warning("ATTENTION:\n"
×
214
            + "Your user specific PATH variable seems to be empty.\n"
215
            + "You can double check this by pressing [Windows][r] and launch the program SystemPropertiesAdvanced.\n"
216
            + "Then click on 'Environment variables' and check if 'PATH' is set in the 'user variables' from the upper list.\n"
217
            + "In case 'PATH' is defined there non-empty and you get this message, please abort and give us feedback:\n"
218
            + "https://github.com/devonfw/IDEasy/issues\n"
219
            + "Otherwise all is correct and you can continue.");
220
        this.context.askToContinue("Are you sure you want to override your PATH?");
×
221
      } else {
222
        path.removeEntries(s -> s.endsWith(IDE_BIN));
7✔
223
      }
224
      path.getEntries().add(ideasyBinPath.toString());
6✔
225
      helper.setUserEnvironmentValue(IdeVariables.PATH.getName(), path.toString());
6✔
226
      setGitLongpaths();
2✔
227
    }
228
  }
1✔
229

230
  private void setGitLongpaths() {
231
    GitContext gitContext = new GitContextImpl(this.context);
6✔
232
    gitContext.verifyGitInstalled();
2✔
233
    Path configPath = this.context.getUserHome().resolve(".gitconfig");
6✔
234
    IniFile iniFile = new IniFileImpl();
4✔
235
    FileAccess fileAccess = new FileAccessImpl(context);
6✔
236
    fileAccess.readIniFile(configPath, iniFile);
4✔
237
    IniSection coreSection = iniFile.getOrCreateSection("core");
4✔
238
    coreSection.getProperties().put("longpaths", "true");
6✔
239
    fileAccess.writeIniFile(iniFile, configPath);
4✔
240
  }
1✔
241

242
  static String removeObsoleteEntryFromWindowsPath(String userPath) {
243
    return removeEntryFromWindowsPath(userPath, IDE_BIN);
4✔
244
  }
245

246
  static String removeEntryFromWindowsPath(String userPath, String suffix) {
247
    int len = userPath.length();
3✔
248
    int start = 0;
2✔
249
    while ((start >= 0) && (start < len)) {
5!
250
      int end = userPath.indexOf(';', start);
5✔
251
      if (end < 0) {
2✔
252
        end = len;
2✔
253
      }
254
      String entry = userPath.substring(start, end);
5✔
255
      if (entry.endsWith(suffix)) {
4✔
256
        String prefix = "";
2✔
257
        int offset = 1;
2✔
258
        if (start > 0) {
2✔
259
          prefix = userPath.substring(0, start - 1);
7✔
260
          offset = 0;
2✔
261
        }
262
        if (end == len) {
3✔
263
          return prefix;
2✔
264
        } else {
265
          return removeEntryFromWindowsPath(prefix + userPath.substring(end + offset), suffix);
10✔
266
        }
267
      }
268
      start = end + 1;
4✔
269
    }
1✔
270
    return userPath;
2✔
271
  }
272

273
  /**
274
   * Adds ourselves to the shell RC (run-commands) configuration file.
275
   *
276
   * @param filename the name of the RC file.
277
   * @param ideRoot the IDE_ROOT {@link Path}.
278
   */
279
  private void addToShellRc(String filename, Path ideRoot, String extraLine) {
280

281
    modifyShellRc(filename, ideRoot, true, extraLine);
6✔
282
  }
1✔
283

284
  private void removeFromShellRc(String filename, Path ideRoot) {
285

286
    modifyShellRc(filename, ideRoot, false, null);
6✔
287
  }
1✔
288

289
  /**
290
   * Adds ourselves to the shell RC (run-commands) configuration file.
291
   *
292
   * @param filename the name of the RC file.
293
   * @param ideRoot the IDE_ROOT {@link Path}.
294
   */
295
  private void modifyShellRc(String filename, Path ideRoot, boolean add, String extraLine) {
296

297
    if (add) {
2✔
298
      this.context.info("Configuring IDEasy in {}", filename);
11✔
299
    } else {
300
      this.context.info("Removing IDEasy from {}", filename);
10✔
301
    }
302
    Path rcFile = this.context.getUserHome().resolve(filename);
6✔
303
    FileAccess fileAccess = this.context.getFileAccess();
4✔
304
    List<String> lines = fileAccess.readFileLines(rcFile);
4✔
305
    if (lines == null) {
2✔
306
      if (!add) {
2!
307
        return;
×
308
      }
309
      lines = new ArrayList<>();
5✔
310
    } else {
311
      // since it is unspecified if the returned List may be immutable we want to get sure
312
      lines = new ArrayList<>(lines);
5✔
313
    }
314
    Iterator<String> iterator = lines.iterator();
3✔
315
    int removeCount = 0;
2✔
316
    while (iterator.hasNext()) {
3✔
317
      String line = iterator.next();
4✔
318
      line = line.trim();
3✔
319
      if (isObsoleteRcLine(line)) {
3✔
320
        this.context.info("Removing obsolete line from {}: {}", filename, line);
14✔
321
        iterator.remove();
2✔
322
        removeCount++;
2✔
323
      } else if (line.equals(extraLine)) {
4✔
324
        extraLine = null;
2✔
325
      }
326
    }
1✔
327
    if (add) {
2✔
328
      if (extraLine != null) {
2!
329
        lines.add(extraLine);
×
330
      }
331
      if (!this.context.getSystemInfo().isWindows()) {
5✔
332
        lines.add("export IDE_ROOT=\"" + WindowsPathSyntax.MSYS.format(ideRoot) + "\"");
7✔
333
      }
334
      lines.add(BASH_CODE_SOURCE_FUNCTIONS);
4✔
335
    }
336
    fileAccess.writeFileLines(lines, rcFile);
4✔
337
    this.context.debug("Successfully updated {}", filename);
10✔
338
  }
1✔
339

340
  private static boolean isObsoleteRcLine(String line) {
341
    if (line.startsWith("alias ide=")) {
4!
342
      return true;
×
343
    } else if (line.startsWith("export IDE_ROOT=")) {
4!
344
      return true;
×
345
    } else if (line.equals("ide")) {
4✔
346
      return true;
2✔
347
    } else if (line.equals("ide init")) {
4✔
348
      return true;
2✔
349
    } else if (line.startsWith("source \"$IDE_ROOT/_ide/")) {
4✔
350
      return true;
2✔
351
    }
352
    return false;
2✔
353
  }
354

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

357
    Path artifactPath = cwd.resolve(artifactName);
4✔
358
    if (Files.exists(artifactPath)) {
5!
359
      installationArtifacts.add(artifactPath);
5✔
360
    } else if (required) {
×
361
      this.context.error("Missing required file {}", artifactName);
×
362
      return false;
×
363
    }
364
    return true;
2✔
365
  }
366

367
  private Path determineIdeRoot(Path cwd) {
368
    Path ideRoot = this.context.getIdeRoot();
4✔
369
    if (ideRoot == null) {
2!
370
      Path home = this.context.getUserHome();
4✔
371
      Path installRoot = home;
2✔
372
      if (this.context.getSystemInfo().isWindows()) {
5✔
373
        if (!cwd.startsWith(home)) {
4!
374
          installRoot = cwd.getRoot();
×
375
        }
376
      }
377
      ideRoot = installRoot.resolve(IdeContext.FOLDER_PROJECTS);
4✔
378
    } else {
1✔
379
      assert (Files.isDirectory(ideRoot)) : "IDE_ROOT directory does not exist!";
×
380
    }
381
    return ideRoot;
2✔
382
  }
383

384
  /**
385
   * Uninstalls IDEasy entirely from the system.
386
   */
387
  public void uninstallIdeasy() {
388

389
    Path ideRoot = this.context.getIdeRoot();
4✔
390
    removeFromShellRc(BASHRC, ideRoot);
4✔
391
    removeFromShellRc(ZSHRC, ideRoot);
4✔
392
    Path idePath = this.context.getIdePath();
4✔
393
    uninstallIdeasyWindowsEnv(ideRoot);
3✔
394
    uninstallIdeasyIdePath(idePath);
3✔
395
    deleteDownloadCache();
2✔
396
    this.context.success("IDEasy has been uninstalled from your system.");
4✔
397
    this.context.interaction("ATTENTION:\n"
10✔
398
        + "In order to prevent data-loss, we do not delete your projects and git repositories!\n"
399
        + "To entirely get rid of IDEasy, also check your IDE_ROOT folder at:\n"
400
        + "{}", ideRoot);
401
  }
1✔
402

403
  private void deleteDownloadCache() {
404
    Path downloadPath = this.context.getDownloadPath();
4✔
405
    this.context.info("Deleting download cache from {}", downloadPath);
10✔
406
    this.context.getFileAccess().delete(downloadPath);
5✔
407
  }
1✔
408

409
  private void uninstallIdeasyIdePath(Path idePath) {
410
    if (this.context.getSystemInfo().isWindows()) {
5✔
411
      this.context.newProcess().executable("bash").addArgs("-c",
17✔
412
          "sleep 10 && rm -rf \"" + WindowsPathSyntax.MSYS.format(idePath) + "\"").run(ProcessMode.BACKGROUND);
5✔
413
      this.context.interaction("To prevent windows file locking errors, we perform an asynchronous deletion of {} in background now.\n"
11✔
414
          + "Please close all terminals and wait a minute for the deletion to complete before running other commands.", idePath);
415
    } else {
416
      this.context.info("Finally deleting {}", idePath);
10✔
417
      this.context.getFileAccess().delete(idePath);
5✔
418
    }
419
  }
1✔
420

421
  private void uninstallIdeasyWindowsEnv(Path ideRoot) {
422
    if (!this.context.getSystemInfo().isWindows()) {
5✔
423
      return;
1✔
424
    }
425
    WindowsHelper helper = WindowsHelper.get(this.context);
4✔
426
    helper.removeUserEnvironmentValue(IdeVariables.IDE_ROOT.getName());
4✔
427
    String userPath = helper.getUserEnvironmentValue(IdeVariables.PATH.getName());
5✔
428
    if (userPath == null) {
2!
429
      this.context.error("Could not read user PATH from registry!");
×
430
    } else {
431
      this.context.info("Found user PATH={}", userPath);
10✔
432
      String newUserPath = userPath;
2✔
433
      if (!userPath.isEmpty()) {
3!
434
        SimpleSystemPath path = SimpleSystemPath.of(userPath, ';');
4✔
435
        path.removeEntries(s -> s.endsWith(IDE_BIN) || s.endsWith(IDE_INSTALLATION_BIN));
15!
436
        newUserPath = path.toString();
3✔
437
      }
438
      if (newUserPath.equals(userPath)) {
4!
439
        this.context.error("Could not find IDEasy in PATH:\n{}", userPath);
×
440
      } else {
441
        helper.setUserEnvironmentValue(IdeVariables.PATH.getName(), newUserPath);
5✔
442
      }
443
    }
444
  }
1✔
445
}
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