diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 74e0e5bb3b..f60cfac82b 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -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: 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 0ccceabbcb..409fda70a8 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 @@ -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 args = buildMvnArgs(installationPath, pomPath); + List 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 @@ -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 buildMvnArgs(Path installationPath, Path pomPath) { + static List buildMvnArgs(Path installationPath, Path pomPath, String execExecutable) { List 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" @@ -134,4 +135,48 @@ static List buildMvnArgs(Path installationPath, Path pomPath) { } return args; } + + private static final String GUI_INFO_PLIST = """ + + + + + CFBundleExecutable + IDEasy + CFBundleIdentifier + com.devonfw.tools.ideasy.gui + CFBundleName + IDEasy + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + + + """; + + /** + * 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(); + } } 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 index 95335cbe07..16b122677d 100644 --- 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 @@ -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(); @@ -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("CFBundleName", "IDEasy", + "CFBundleIdentifier"); + } + + @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. @@ -42,7 +117,7 @@ void testBuildMvnArgsWithLocalDevBuild() { Path installationPath = this.tempDir; // act - List args = Gui.buildMvnArgs(installationPath, installationPath.resolve("gui/pom.xml")); + List args = Gui.buildMvnArgs(installationPath, installationPath.resolve("gui/pom.xml"), "java"); // assert String m2RepoFlag = "-Dmaven.repo.local=" + installationPath.resolve(".m2"); @@ -62,7 +137,7 @@ void testBuildMvnArgsWithSnapshotVersion() throws IOException { String m2RepoFlag = "-Dmaven.repo.local=" + installationPath.resolve(".m2"); // act - List args = Gui.buildMvnArgs(installationPath, installationPath.resolve("gui/pom.xml")); + List args = Gui.buildMvnArgs(installationPath, installationPath.resolve("gui/pom.xml"), "java"); // assert assertThat(args).contains("-U"); @@ -82,11 +157,26 @@ void testBuildMvnArgsWithStableVersion() { String m2RepoFlag = "-Dmaven.repo.local=" + installationPath.resolve(".m2"); // act - List args = Gui.buildMvnArgs(installationPath, installationPath.resolve("gui/pom.xml")); + List 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 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"); + } + }