From 7e0654fc534605f6aae006bfbbcb2c4fda03bce0 Mon Sep 17 00:00:00 2001 From: Dominic Letz Date: Tue, 1 Sep 2026 11:03:32 +0200 Subject: [PATCH 1/5] Skip release-first erl .env for Windows host-first. Host-first starts BEAM via bin/.bat, which already passes -boot. Installing rel/win32/app.env.eex onto the renamed erl embeds a second -boot in ERL_AFLAGS, so OTP exits with Conflicting -boot options and the GUI host looks dead on click. Co-authored-by: Cursor --- lib/package.ex | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/package.ex b/lib/package.ex index 707546c..1e278b4 100644 --- a/lib/package.ex +++ b/lib/package.ex @@ -106,8 +106,12 @@ defmodule Desktop.Deployment.Package do build_root = Path.join([rel_path, "..", ".."]) |> Path.expand() File.write!(Path.join(build_root, "app.exe.manifest"), content) - # fetch extra env file - if File.exists?("rel/win32/app.env.eex") do + # Release-first embeds boot/heart flags in `.exe.env` so the renamed + # erl can be launched directly. Host-first starts via `bin/.bat`, which + # already passes `-boot`; writing those same flags into `.env` makes OTP + # exit immediately with "Conflicting -boot options" and the GUI host looks + # like a no-op click. + if pkg.windows_layout != :host_first and File.exists?("rel/win32/app.env.eex") do content = eval_eex("rel/win32/app.env.eex", rel, pkg) File.write!(new_name <> ".env", content) end From 86b58408e4b6e04fb4db272782ed6b1ed44b3646 Mon Sep 17 00:00:00 2001 From: Dominic Letz Date: Tue, 1 Sep 2026 14:48:54 +0200 Subject: [PATCH 2/5] Also delete leftover erl .env on host-first Windows builds. Skipping the write was not enough when a prior packaging left dDrive.exe.env in erts/bin; remove it explicitly for host_first. Co-authored-by: Cursor --- lib/package.ex | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/package.ex b/lib/package.ex index 1e278b4..6e6a8e7 100644 --- a/lib/package.ex +++ b/lib/package.ex @@ -110,10 +110,16 @@ defmodule Desktop.Deployment.Package do # erl can be launched directly. Host-first starts via `bin/.bat`, which # already passes `-boot`; writing those same flags into `.env` makes OTP # exit immediately with "Conflicting -boot options" and the GUI host looks - # like a no-op click. - if pkg.windows_layout != :host_first and File.exists?("rel/win32/app.env.eex") do - content = eval_eex("rel/win32/app.env.eex", rel, pkg) - File.write!(new_name <> ".env", content) + # like a no-op click. Also remove any leftover `.env` from a prior build. + env_file = new_name <> ".env" + + if pkg.windows_layout == :host_first do + File.rm(env_file) + else + if File.exists?("rel/win32/app.env.eex") do + content = eval_eex("rel/win32/app.env.eex", rel, pkg) + File.write!(env_file, content) + end end git_version = From b4be9b2fde7b7691cb313689c01663bb4709f4c0 Mon Sep 17 00:00:00 2001 From: Dominic Letz Date: Tue, 1 Sep 2026 15:16:38 +0200 Subject: [PATCH 3/5] Embed package icon into the Windows host-first exe. Host-first only applied Pe.Update to the renamed erts erl binary, so Explorer/taskbar showed a blank default icon for dDrive.exe. Copy the host then --set-icon from the release icon.ico. Co-authored-by: Cursor --- lib/package/windows.ex | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/package/windows.ex b/lib/package/windows.ex index b096803..3dea988 100644 --- a/lib/package/windows.ex +++ b/lib/package/windows.ex @@ -35,15 +35,27 @@ defmodule Desktop.Deployment.Package.Windows do host_bin = resolve_host_binary!(pkg) host_name = host_install_name(pkg, host_bin) - File.cp!(host_bin, Path.join(package_root, host_name)) + host_dest = Path.join(package_root, host_name) + File.cp!(host_bin, host_dest) IO.puts("Host-first Windows/#{host_name} <- #{host_bin}") + # Explorer/taskbar use the host PE icon. copy_extra_files embeds the icon into + # the renamed erts erl.exe, but the native host is a separate copy and must + # be updated here as well. + icon_rel = Path.join(["lib", "#{pkg.app_name}-#{vsn}", "priv", "icon.ico"]) + icon = Path.join(path, icon_rel) + + if File.exists?(icon) do + :ok = Mix.Tasks.Pe.Update.run(["--set-icon", icon, host_dest]) + IO.puts("Host-first Windows/#{host_name} icon <- #{icon}") + else + Mix.shell().error("Host-first packaging: missing icon at #{icon}") + end + write_host_ini!(package_root, host_name, release_subdir, windows_beam_app_name(pkg)) copy_release_tree!(path, beam_root) move_redistributables!(beam_root, package_root) - icon_rel = Path.join(["lib", "#{pkg.app_name}-#{vsn}", "priv", "icon.ico"]) - pkg = pkg |> put_priv(:windows_layout, :host_first) From a9ee5b93eeea679ab66d797bca299bad1dc9fef2 Mon Sep 17 00:00:00 2001 From: Dominic Letz Date: Tue, 1 Sep 2026 16:17:01 +0200 Subject: [PATCH 4/5] Fix Windows shortcut icons eaten by NSIS backslash escapes. Host-first shortcuts now use the embedded host PE icon; other icon paths are normalized to forward slashes so CreateShortCut keeps separators. Co-authored-by: Cursor --- lib/package/windows.ex | 12 ++++++++++-- rel/win32/app.nsi.eex | 9 +++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/lib/package/windows.ex b/lib/package/windows.ex index 3dea988..9613a1b 100644 --- a/lib/package/windows.ex +++ b/lib/package/windows.ex @@ -56,12 +56,15 @@ defmodule Desktop.Deployment.Package.Windows do copy_release_tree!(path, beam_root) move_redistributables!(beam_root, package_root) + icon_rel = Path.join(release_subdir, icon_rel) + pkg = pkg |> put_priv(:windows_layout, :host_first) |> put_priv(:host_executable, host_name) |> put_priv(:release_subdir, release_subdir) - |> put_priv(:icon_rel, Path.join(release_subdir, icon_rel)) + |> put_priv(:icon_rel, nsis_path(icon_rel)) + |> put_priv(:mui_icon, nsis_path(Path.join(package_root, icon_rel))) |> put_priv(:package_root, package_root) |> put_priv(:nsi_outfile, "../#{pkg.name}-#{vsn}.exe") |> put_priv(:scheme_launcher, "\"${TARGET}\" \"%1\"") @@ -152,7 +155,8 @@ defmodule Desktop.Deployment.Package.Windows do pkg = pkg |> put_priv(:windows_layout, :release_first) - |> put_priv(:icon_rel, icon_rel) + |> put_priv(:icon_rel, nsis_path(icon_rel)) + |> put_priv(:mui_icon, nsis_path(Path.join(path, icon_rel))) |> put_priv(:package_root, path) |> put_priv(:nsi_outfile, "../../#{pkg.name}-#{vsn}.exe") |> put_priv( @@ -251,6 +255,10 @@ defmodule Desktop.Deployment.Package.Windows do Enum.uniq(bins) end + # NSIS treats `\` as an escape in strings. Paths embedded in the .nsi must use + # `/` so CreateShortCut IconLocation and MUI_ICON keep their separators. + defp nsis_path(path), do: String.replace(path, "\\", "/") + defp put_priv(pkg, key, value) do %{pkg | priv: Map.put(pkg.priv, key, value)} end diff --git a/rel/win32/app.nsi.eex b/rel/win32/app.nsi.eex index 2f7de47..f32e0d6 100644 --- a/rel/win32/app.nsi.eex +++ b/rel/win32/app.nsi.eex @@ -25,12 +25,13 @@ InstallDir "$PROGRAMFILES64\<%= @package.name_long || @package.name %>" <%= if @package.priv[:windows_layout] == :host_first do %> !define TARGET "$INSTDIR\<%= @package.priv.host_executable %>" !define TARGET_PARAMS "" -!define TARGET_ICON "$INSTDIR\<%= @package.priv.icon_rel %>" +; Host PE embeds the package icon; shortcuts should use that, not a priv .ico path. +!define TARGET_ICON "${TARGET}" !define KILL_HOST "<%= @package.priv.host_executable %>" <% else %> !define TARGET "$INSTDIR\run.vbs" !define TARGET_PARAMS "" -!define TARGET_ICON "$INSTDIR\<%= @package.priv.icon_rel %>" +!define TARGET_ICON "$INSTDIR/<%= @package.priv.icon_rel %>" !define KILL_HOST "heart.exe" <% end %> @@ -41,8 +42,8 @@ InstallDirRegKey HKLM "${MY_STARTMENUPAGE_REGISTRY_KEY}" "" !define MUI_STARTMENUPAGE_DEFAULTFOLDER "<%= @package.name_long || @package.name %>" -!define MUI_ICON "<%= @package.priv.package_root %>\<%= @package.priv.icon_rel %>" -!define MUI_UNICON "<%= @package.priv.package_root %>\<%= @package.priv.icon_rel %>" +!define MUI_ICON "<%= @package.priv.mui_icon %>" +!define MUI_UNICON "<%= @package.priv.mui_icon %>" ;-------------------------------- ;Modern UI Configuration From b05ada72bd56b894fcd53484a6c0167462dd10bc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 1 Sep 2026 19:38:32 +0000 Subject: [PATCH 5/5] Refactor Windows copy_extra_files to satisfy Credo complexity. Extract host-first env-file sync and PE update args into helpers so mix lint stays under the cyclomatic complexity threshold. Co-authored-by: Dominic Letz --- lib/package.ex | 72 +++++++++++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/lib/package.ex b/lib/package.ex index 6e6a8e7..3390198 100644 --- a/lib/package.ex +++ b/lib/package.ex @@ -106,21 +106,7 @@ defmodule Desktop.Deployment.Package do build_root = Path.join([rel_path, "..", ".."]) |> Path.expand() File.write!(Path.join(build_root, "app.exe.manifest"), content) - # Release-first embeds boot/heart flags in `.exe.env` so the renamed - # erl can be launched directly. Host-first starts via `bin/.bat`, which - # already passes `-boot`; writing those same flags into `.env` makes OTP - # exit immediately with "Conflicting -boot options" and the GUI host looks - # like a no-op click. Also remove any leftover `.env` from a prior build. - env_file = new_name <> ".env" - - if pkg.windows_layout == :host_first do - File.rm(env_file) - else - if File.exists?("rel/win32/app.env.eex") do - content = eval_eex("rel/win32/app.env.eex", rel, pkg) - File.write!(env_file, content) - end - end + maybe_sync_windows_erl_env_file(pkg, rel, new_name) git_version = with {version, 0} <- System.cmd("git", ["describe", "--tags", "--always"]) do @@ -145,24 +131,8 @@ defmodule Desktop.Deployment.Package do # Unsafe binary removal of "Erlang", needs same length! file_replace(bin, "Erlang", binary_part(pkg.name <> <<0, 0, 0, 0, 0, 0>>, 0, 6)) - # Host-first: keep the CUI subsystem so OTP 26's user/logger get a console when - # DesktopWebView spawns the release (GUI subsystem causes nouser / invalid handle). - # Release-first: mark GUI so launching erl.exe does not flash a console window. - pe_args = - if pkg.windows_layout == :host_first do - ["--set-icon", icon, "--set-manifest", Path.join(build_root, "app.exe.manifest")] - else - [ - "--set-icon", - icon, - "--set-subsystem", - "IMAGE_SUBSYSTEM_WINDOWS_GUI", - "--set-manifest", - Path.join(build_root, "app.exe.manifest") - ] - end - - :ok = Mix.Tasks.Pe.Update.run(pe_args ++ info ++ [bin]) + :ok = + Mix.Tasks.Pe.Update.run(windows_pe_update_args(pkg, icon, build_root) ++ info ++ [bin]) end [elixir] = wildcard(rel, "**/elixir.bat") @@ -211,6 +181,42 @@ defmodule Desktop.Deployment.Package do pkg end + # Release-first embeds boot/heart flags in `.exe.env` so the renamed + # erl can be launched directly. Host-first starts via `bin/.bat`, which + # already passes `-boot`; writing those same flags into `.env` makes OTP + # exit immediately with "Conflicting -boot options" and the GUI host looks + # like a no-op click. Also remove any leftover `.env` from a prior build. + defp maybe_sync_windows_erl_env_file(%Package{windows_layout: :host_first}, _, new_name) do + File.rm(new_name <> ".env") + end + + defp maybe_sync_windows_erl_env_file(%Package{} = pkg, rel, new_name) do + env_file = new_name <> ".env" + + if File.exists?("rel/win32/app.env.eex") do + content = eval_eex("rel/win32/app.env.eex", rel, pkg) + File.write!(env_file, content) + end + end + + # Host-first: keep the CUI subsystem so OTP 26's user/logger get a console when + # DesktopWebView spawns the release (GUI subsystem causes nouser / invalid handle). + # Release-first: mark GUI so launching erl.exe does not flash a console window. + defp windows_pe_update_args(%Package{windows_layout: :host_first}, icon, build_root) do + ["--set-icon", icon, "--set-manifest", Path.join(build_root, "app.exe.manifest")] + end + + defp windows_pe_update_args(_pkg, icon, build_root) do + [ + "--set-icon", + icon, + "--set-subsystem", + "IMAGE_SUBSYSTEM_WINDOWS_GUI", + "--set-manifest", + Path.join(build_root, "app.exe.manifest") + ] + end + defp copy_extra_files(os, %Package{release: %Mix.Release{} = rel} = pkg) when os == Linux or os == MacOS do [beam] = wildcard(rel, "**/beam.smp")