diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index c3fd5c3c01..eb4f250c06 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -47,6 +47,7 @@ Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/2292[#2292]: Questions appear twice * https://github.com/devonfw/IDEasy/issues/825[#825]: Add commandlet for GC Log Analyzer * https://github.com/devonfw/IDEasy/issues/1938[#1938]: GUI not launchable outside of project context +* 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/48?closed=1[milestone 2026.08.001]. diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/gui/Gui.java b/cli/src/main/java/com/devonfw/tools/ide/tool/gui/Gui.java index b0359c11ad..c22f3917a9 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/gui/Gui.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/gui/Gui.java @@ -1,5 +1,6 @@ package com.devonfw.tools.ide.tool.gui; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -88,7 +89,7 @@ protected void doRun() { "-f", //use specified POM file pomPath.toString(), "org.codehaus.mojo:exec-maven-plugin:3.1.0:exec", - "-Dexec.executable=java", + "-Dexec.executable=" + getGuiExecutable(javaInstallation), "-Dexec.classpathScope=compile", "-Dexec.args=-classpath %classpath com.devonfw.ide.gui.AppLauncher", "-Dexec.async=true" @@ -111,4 +112,59 @@ protected void doRun() { "Failed to launch the GUI. If maven reports issues with dependency resolution, check whether the maven M2 repo is enabled in your project.", e); } } + + private static final String GUI_APP_BUNDLE_ID = "com.devonfw.tools.ideasy.gui"; + + private static final String GUI_INFO_PLIST = """ + + + + + CFBundleExecutable + IDEasy + CFBundleIdentifier + %s + CFBundleName + IDEasy + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + + + """.formatted(GUI_APP_BUNDLE_ID); + + /** + * 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()); + writeFile(contentsDir.resolve("Info.plist"), GUI_INFO_PLIST); + this.context.getFileAccess().symlink(javaExecutable, launcher, false); + return launcher.toString(); + } + + private static void writeFile(Path file, String content) { + + try { + Files.writeString(file, content); + } catch (IOException e) { + throw new IllegalStateException("Failed to write file: " + file, e); + } + } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/gui/GuiTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/gui/GuiTest.java new file mode 100644 index 0000000000..7b6a9235b5 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/gui/GuiTest.java @@ -0,0 +1,91 @@ +package com.devonfw.tools.ide.tool.gui; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.AbstractIdeTestContext; +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.VersionIdentifier; + +/** + * Test of {@link Gui}. + */ +class GuiTest extends AbstractIdeContextTest { + + 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); + ((AbstractIdeTestContext) 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("CFBundleName", "IDEasy", + "CFBundleIdentifier"); + } + + @Test + void testGetGuiExecutableOnWindowsReturnsPlainJava() { + + // arrange + IdeTestContext context = newContext(PROJECT_BASIC, null, true); + ((AbstractIdeTestContext) 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); + ((AbstractIdeTestContext) 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"); + } + +}