Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

This file documents all notable changes to https://github.com/devonfw/IDEasy[IDEasy].

== 2026.09.002

Release with new features and bugfixes:

* https://github.com/devonfw/IDEasy/issues/2206[#2206]: IDEasy title not properly shown on macOS

The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/50?closed=1[milestone 2026.09.002].

== 2026.09.001

Release with new features and bugfixes:
Expand Down
51 changes: 48 additions & 3 deletions cli/src/main/java/com/devonfw/tools/ide/tool/gui/Gui.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ protected void doRun() {
throw new CliException("Fatal error: The pom.xml file required for launching the IDEasy GUI could not be found in expected location: " + pomPath);
}

List<String> args = buildMvnArgs(installationPath, pomPath);
List<String> args = buildMvnArgs(installationPath, pomPath, getGuiExecutable(javaInstallation));

/*
* We manually update the PATH entry with our java version, as by default IDEasy includes the SymLink under /projectname/software/java/bin in the PATH
Expand All @@ -110,15 +110,16 @@ protected void doRun() {
*
* @param installationPath the {@link IdeContext#getIdeInstallationPath() IDEasy installation} directory containing {@code gui/pom.xml}.
* @param pomPath the launcher POM ({@code gui/pom.xml}) to launch the GUI from. Must be verified to exist before this is called.
* @param execExecutable the executable to pass as {@code -Dexec.executable} (see {@link #getGuiExecutable(ToolInstallation)}).
* @return the {@code mvn} arguments to launch the GUI.
*/
static List<String> buildMvnArgs(Path installationPath, Path pomPath) {
static List<String> buildMvnArgs(Path installationPath, Path pomPath, String execExecutable) {

List<String> args = new ArrayList<>(List.of(
"-f", //use specified POM file
pomPath.toString(),
"org.codehaus.mojo:exec-maven-plugin:3.1.0:exec",
"-Dexec.executable=java",
"-Dexec.executable=" + execExecutable,
"-Dexec.classpathScope=compile",
"-Dexec.args=-classpath %classpath com.devonfw.ide.gui.AppLauncher",
"-Dexec.async=true"
Expand All @@ -134,4 +135,48 @@ static List<String> buildMvnArgs(Path installationPath, Path pomPath) {
}
return args;
}

private static final String GUI_INFO_PLIST = """
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>IDEasy</string>
<key>CFBundleIdentifier</key>
<string>com.devonfw.tools.ideasy.gui</string>
<key>CFBundleName</key>
<string>IDEasy</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
</dict>
</plist>
""";

/**
* The GUI is launched as a plain {@code java} process without a native app bundle. On macOS this makes the Dock and menu bar show the executable's
* filename "java" instead of "IDEasy" (see issue #2206). Merely renaming/symlinking the bare executable is not reliable: without a real bundle,
* LaunchServices may still fall back to the underlying JDK's own identity. Instead, we wrap the launch in a minimal {@code .app} bundle: a proper
* {@code Info.plist} declaring our own {@code CFBundleName}/{@code CFBundleIdentifier}, with {@code Contents/MacOS/IDEasy} as a symlink pointing
* directly at the real java executable (not a wrapper script - an intermediate {@code exec} would replace the process image and lose the bundle
* identity again).
*
* @param javaInstallation the {@link ToolInstallation} of the java tool used to launch the GUI.
* @return the executable to pass to Maven's {@code exec:exec} goal in order to launch the GUI.
*/
String getGuiExecutable(ToolInstallation javaInstallation) {

if (!this.context.getSystemInfo().isMac()) {
return "java";
}
Path javaExecutable = javaInstallation.binDir().resolve("java");
Path contentsDir = this.context.getTempPath().resolve("IDEasy.app").resolve("Contents");
Path launcher = contentsDir.resolve("MacOS").resolve("IDEasy");
this.context.getFileAccess().mkdirs(launcher.getParent());
this.context.getFileAccess().writeFileContent(GUI_INFO_PLIST, contentsDir.resolve("Info.plist"));
this.context.getFileAccess().symlink(javaExecutable, launcher, false);
return launcher.toString();
}
}
100 changes: 95 additions & 5 deletions cli/src/test/java/com/devonfw/tools/ide/tool/gui/GuiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,22 @@
import java.nio.file.Path;
import java.util.List;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import com.devonfw.tools.ide.context.AbstractIdeContextTest;
import com.devonfw.tools.ide.context.IdeTestContext;
import com.devonfw.tools.ide.io.WindowsSymlinkTestHelper;
import com.devonfw.tools.ide.os.SystemInfoMock;
import com.devonfw.tools.ide.tool.ToolInstallation;
import com.devonfw.tools.ide.version.IdeVersion;
import com.devonfw.tools.ide.version.VersionIdentifier;

/**
* Test of {@link Gui}.
*/
class GuiTest extends Assertions {
class GuiTest extends AbstractIdeContextTest {

/** Version in effect before the tests so it can be restored by {@link #restoreVersion()}. */
private final String originalVersion = IdeVersion.getVersionString();
Expand All @@ -30,6 +35,76 @@ void restoreVersion() {
IdeVersion.setMockVersionForTesting(this.originalVersion);
}

private ToolInstallation newJavaInstallation(IdeTestContext context) {

Path binDir = context.getSoftwarePath().resolve("java/bin");
context.getFileAccess().mkdirs(binDir);
Path javaExecutable = binDir.resolve("java");
try {
Files.createFile(javaExecutable);
} catch (Exception e) {
throw new IllegalStateException(e);
}
return new ToolInstallation(binDir.getParent(), binDir.getParent(), binDir, VersionIdentifier.of("25.0.0"), false);
}

@Test
void testGetGuiExecutableOnMacCreatesAppBundleWithSymlinkNamedIDEasy() throws Exception {

// arrange
WindowsSymlinkTestHelper.assumeSymlinksSupported();
IdeTestContext context = newContext(PROJECT_BASIC, null, true);
context.setSystemInfo(SystemInfoMock.MAC_ARM64);
ToolInstallation javaInstallation = newJavaInstallation(context);
Gui gui = new Gui(context);

// act
String executable = gui.getGuiExecutable(javaInstallation);

// assert
Path contentsDir = context.getTempPath().resolve("IDEasy.app").resolve("Contents");
Path launcher = contentsDir.resolve("MacOS").resolve("IDEasy");
assertThat(executable).isEqualTo(launcher.toString());
assertThat(Files.isSymbolicLink(launcher)).isTrue();
assertThat(launcher.toRealPath()).isEqualTo(javaInstallation.binDir().resolve("java").toRealPath());
Path infoPlist = contentsDir.resolve("Info.plist");
assertThat(infoPlist).exists();
assertThat(Files.readString(infoPlist)).contains("<key>CFBundleName</key>", "<string>IDEasy</string>",
"<key>CFBundleIdentifier</key>");
}

@Test
void testGetGuiExecutableOnWindowsReturnsPlainJava() {

// arrange
IdeTestContext context = newContext(PROJECT_BASIC, null, true);
context.setSystemInfo(SystemInfoMock.WINDOWS_X64);
ToolInstallation javaInstallation = newJavaInstallation(context);
Gui gui = new Gui(context);

// act
String executable = gui.getGuiExecutable(javaInstallation);

// assert
assertThat(executable).isEqualTo("java");
}

@Test
void testGetGuiExecutableOnLinuxReturnsPlainJava() {

// arrange
IdeTestContext context = newContext(PROJECT_BASIC, null, true);
context.setSystemInfo(SystemInfoMock.LINUX_X64);
ToolInstallation javaInstallation = newJavaInstallation(context);
Gui gui = new Gui(context);

// act
String executable = gui.getGuiExecutable(javaInstallation);

// assert
assertThat(executable).isEqualTo("java");
}

/**
* Verifies the GUI is launched from the self-contained maven repository inside the installation when the running version is a local-dev build (stamped with
* {@link IdeVersion#LOCAL_DEV_SUFFIX}), and that the snapshot (-U) behavior is not applied in that case.
Expand All @@ -42,7 +117,7 @@ void testBuildMvnArgsWithLocalDevBuild() {
Path installationPath = this.tempDir;

// act
List<String> args = Gui.buildMvnArgs(installationPath, installationPath.resolve("gui/pom.xml"));
List<String> args = Gui.buildMvnArgs(installationPath, installationPath.resolve("gui/pom.xml"), "java");

// assert
String m2RepoFlag = "-Dmaven.repo.local=" + installationPath.resolve(".m2");
Expand All @@ -62,7 +137,7 @@ void testBuildMvnArgsWithSnapshotVersion() throws IOException {
String m2RepoFlag = "-Dmaven.repo.local=" + installationPath.resolve(".m2");

// act
List<String> args = Gui.buildMvnArgs(installationPath, installationPath.resolve("gui/pom.xml"));
List<String> args = Gui.buildMvnArgs(installationPath, installationPath.resolve("gui/pom.xml"), "java");

// assert
assertThat(args).contains("-U");
Expand All @@ -82,11 +157,26 @@ void testBuildMvnArgsWithStableVersion() {
String m2RepoFlag = "-Dmaven.repo.local=" + installationPath.resolve(".m2");

// act
List<String> args = Gui.buildMvnArgs(installationPath, installationPath.resolve("gui/pom.xml"));
List<String> args = Gui.buildMvnArgs(installationPath, installationPath.resolve("gui/pom.xml"), "java");

// assert
assertThat(args).doesNotContain("-U");
assertThat(args).doesNotContain(m2RepoFlag);
assertThat(args).doesNotContain("-o");
}

@Test
void testBuildMvnArgsUsesGivenExecutable() {

// arrange
Path installationPath = this.tempDir;
IdeVersion.setMockVersionForTesting("2026.08.001");

// act
List<String> args = Gui.buildMvnArgs(installationPath, installationPath.resolve("gui/pom.xml"), "/tmp/IDEasy.app/Contents/MacOS/IDEasy");

// assert
assertThat(args).contains("-Dexec.executable=/tmp/IDEasy.app/Contents/MacOS/IDEasy");
}

}