Skip to content
Merged
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
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,49 @@ end

### Windows -> NSIS

All builds (specifically NIFs) are built using msys2, because it's mostly linux compatible but runs natively on windows without any helper libraries.
**Default packaging uses host-first layout** (native host binary owns the process; Mix release is a child under `beam/`):

```
MyApp/ # NSIS $INSTDIR
MyApp.exe # native host (package.name.exe / package.host_executable)
MyApp.ini # generated host config (<exe_basename>.ini)
beam/ # Mix release (package.release_subdir, default "beam")
bin/<app>...
erts-.../
lib/...
releases/...
MicrosoftEdgeWebview2Setup.exe
vcredist_x64.exe
```

Locate the host binary via (first match wins):

1. `package.host_binary`
2. `DESKTOP_HOST_BINARY` (or legacy `DESKTOP_WEBVIEW_BINARY`)
3. Convention discovery: any Mix dependency that ships `priv/native/windows/<executable>.exe`

The installed host filename defaults to `package.name` + `.exe` (override with
`package.host_executable`). Deployment does not hardcode a particular webview
package. Apps that use elixir-desktop/webview simply ship that binary under the
convention above (or set `host_binary`).

Set `windows_layout: :release_first` in `package()` to keep the legacy flat Mix release with `run.vbs`/`run.bat` + heart (OTP `:wx`). Deprecated alias: `webview_backend: :wx`.

All builds (specifically NIFs) are built using msys2, because it's mostly linux compatible but runs natively on windows without any helper libraries.

0) Installing prerequsites
- msys2.org
- `pacman -S mingw-w64-x86_64-imagemagick mingw-w64-x86_64-nsis mingw-w64-x86_64-nsis mingw-w64-x86_64-osslsigncode mingw-w64-x86_64-openssl`

1) `mix deployment` will generate the release binaries

To support windows code signing the user has to create two certificate files `app_key.pem` and `app_key.pem` (e.g. get from sectigo) and put them into the `rel/win32/` subdirectory.
Windows PE metadata (default icon, manifest, version info, checksum) is applied with **libpe** (`mix pe.update`). ImageMagick (`magick convert`) still resizes `package.icon` (PNG) to `icon.ico` before embedding. WinRun4J `rcedit.exe` is no longer used.

To support windows code signing the user has to create two certificate files `app_key.pem` and `app_key.pem` (e.g. get from sectigo) and put them into the `rel/win32/` subdirectory.

#### Known Issues / Comments

* The `.vbs` file is used as indirection for the `.bat` file as it avoid creating a black terminal screen that otherwise flashes shortly when launchin a `.bat` file directly
* For `:release_first`, the `.vbs` file is used as indirection for the `.bat` file as it avoid creating a black terminal screen that otherwise flashes shortly when launchin a `.bat` file directly

* The `.nsis` file currently registers a `app://` protocol handler, this is example use and can be removed for other apps.

Expand Down
29 changes: 18 additions & 11 deletions lib/deployment.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,31 @@ defmodule Desktop.Deployment do
defp expand_package_opts(opts) when is_list(opts) do
opts = Map.new(opts)

layout =
default_layout =
case Map.get(opts, :webview_backend) do
:wx -> :release_first
:desktop_webview -> :host_first
_ -> :host_first
end

macos_layout =
case Map.fetch(opts, :macos_layout) do
{:ok, layout} ->
layout

:error ->
case Map.get(opts, :webview_backend) do
:wx -> :release_first
:desktop_webview -> :host_first
_ -> :host_first
end
{:ok, layout} -> layout
:error -> default_layout
end

windows_layout =
case Map.fetch(opts, :windows_layout) do
{:ok, layout} -> layout
:error -> default_layout
end

host_binary = Map.get(opts, :host_binary) || Map.get(opts, :webview_binary)

opts
|> Map.drop([:webview_backend, :webview_binary])
|> Map.put(:macos_layout, layout)
|> Map.put(:macos_layout, macos_layout)
|> Map.put(:windows_layout, windows_layout)
|> Map.put(:host_binary, host_binary)
|> Map.to_list()
end
Expand Down
72 changes: 33 additions & 39 deletions lib/package.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,21 @@ defmodule Desktop.Deployment.Package do
# Contents/Resources/<release_subdir>/ (e.g. beam/)
# :release_first — Mix release under Contents/Resources/, optional MacOS/run
macos_layout: :host_first,
# Windows NSIS $INSTDIR layout:
# :host_first — native host exe + ini at package root, Mix release under
# <release_subdir>/ (e.g. beam/); no run.vbs/heart
# :release_first — flat Mix release with run.vbs/run.bat + heart (legacy wx)
windows_layout: :host_first,
# Absolute path to the native host binary (host_first). Prefer
# package.host_binary or DESKTOP_HOST_BINARY; otherwise any Mix dep that
# ships priv/native/macos/<exe> is discovered by convention.
# ships priv/native/macos/<exe> or priv/native/windows/<exe>.exe is
# discovered by convention.
host_binary: nil,
# Basename to install as Contents/MacOS/<name> (defaults to host basename)
# Basename to install as MacOS/<name> or <name>.exe at package root
# (defaults to host basename)
host_executable: nil,
# Subdirectory under Contents/Resources for the Mix release (host_first)
# Subdirectory for the Mix release under Resources/ (macOS) or package
# root (Windows) when using host_first
release_subdir: "beam",
# OTP app name written into the host config for starting the release
beam_app_name: nil,
Expand Down Expand Up @@ -103,15 +111,22 @@ defmodule Desktop.Deployment.Package do
file_replace(bin, "Erlang", binary_part(pkg.name <> <<0, 0, 0, 0, 0, 0>>, 0, 6))
cmd!(toolpath("rel/win32/rcedit.exe"), ["/I", bin, icon])

:ok =
Mix.Tasks.Pe.Update.run(
# 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-manifest", Path.join(build_root, "app.exe.manifest")]
else
[
"--set-subsystem",
"IMAGE_SUBSYSTEM_WINDOWS_GUI",
"--set-manifest",
Path.join(build_root, "app.exe.manifest")
] ++ info ++ [bin]
)
]
end

:ok = Mix.Tasks.Pe.Update.run(pe_args ++ info ++ [bin])
end

[elixir] = wildcard(rel, "**/elixir.bat")
Expand Down Expand Up @@ -146,9 +161,16 @@ defmodule Desktop.Deployment.Package do
File.rm!(name)
end

cp!(toolpath("rel/win32/run.vbs"), rel_path)
content = eval_eex(toolpath("rel/win32/run.bat.eex"), rel, pkg)
File.write!(Path.join(rel_path, "run.bat"), content)
# :release_first uses run.vbs/run.bat + heart; :host_first owns the process.
pkg =
if pkg.windows_layout == :release_first do
cp!(toolpath("rel/win32/run.vbs"), rel_path)
content = eval_eex(toolpath("rel/win32/run.bat.eex"), rel, pkg)
File.write!(Path.join(rel_path, "run.bat"), content)
pkg
else
pkg
end

pkg
end
Expand Down Expand Up @@ -187,36 +209,8 @@ defmodule Desktop.Deployment.Package do
case os() do
MacOS -> Package.MacOS.release(pkg)
Linux -> linux_release(pkg)
Windows -> windows_release(pkg)
end
end

defp windows_release(%Package{release: %Mix.Release{path: rel_path, version: vsn} = rel} = pkg) do
build_root = Path.join([rel_path, "..", ".."]) |> Path.expand()
signfun = win32_sign_function(pkg)

if signfun == nil do
Mix.Shell.IO.info("Not signing secret detected. Skipping signing")
else
win32_codesign(signfun, build_root)
Windows -> Package.Windows.release(pkg)
end

nsi_file = toolpath("rel/win32/app.nsi.eex")
{:ok, cur} = :file.get_cwd()
:file.set_cwd(String.to_charlist(rel_path))

content = eval_eex(nsi_file, rel, pkg)
File.write!(Path.join(build_root, "app.nsi"), content)
cmd!("makensis", ["-NOCD", "-DVERSION=#{vsn}", Path.join(build_root, "app.nsi")])
:file.set_cwd(cur)
outfile = "#{pkg.name}-#{vsn}.exe"

if signfun != nil do
path = Path.join([build_root, outfile])
signfun.(path)
end

%{pkg | priv: Map.put(pkg.priv, :installer_name, outfile)}
end

defp linux_release(%Package{release: %Mix.Release{path: rel_path, version: vsn} = rel} = pkg) do
Expand Down
Loading
Loading