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
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,26 @@ To support windows code signing the user has to create two certificate files `ap

### MacOS -> DMG

MacOS has two variants ARM (M1,2,3...) and legacy x86 if you want to enable your builds on both platforms the easiest way is to have the CI (e.g. GitHub) have the build done on a x86_64 machine. Then the rosetta compatibility layer will make it runnable on ARM too. For this rosetta is enabled explicitly in the `.plist` file for ARM machines.
**Default packaging uses DesktopWebView host-first layout** (see elixir-desktop/webview):

```
MyApp.app/Contents/
MacOS/DesktopWebView
Resources/
DesktopWebView.ini
beam/ # Mix release
icons.icns
Info.plist # CFBundleExecutable = DesktopWebView
```

Locate the host binary via (first match wins):

1. `package.webview_binary`
2. `DESKTOP_WEBVIEW_BINARY`
3. `:desktop_webview` app `priv/native/macos/DesktopWebView`
4. `deps/desktop_webview/priv/...` or sibling `../webview/priv/...`

Set `webview_backend: :wx` in `package()` to keep the legacy BEAM-first `MacOS/run` + OTP `:wx` layout (including optional CEF `webview` helper).

Code signing is done as part of the `desktop.installer` task if and only if a developer_id is provided. This can be provided through one of these environment variables:

Expand Down
8 changes: 8 additions & 0 deletions lib/package.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ defmodule Desktop.Deployment.Package do
env: %{},
# import options
import_inofitywait: false,
# macOS DesktopWebView host-first packaging (default on MacOS)
webview_backend: :desktop_webview,
webview_binary: nil,
beam_app_name: nil,
microphone_usage:
"This app may use the microphone for calls and voice features inside the app.",
camera_usage:
"This app may use the camera for calls and video features inside the app.",
# defined during the process
app_name: nil,
release: nil,
Expand Down
198 changes: 163 additions & 35 deletions lib/package/macos.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,81 @@ defmodule Desktop.Deployment.Package.MacOS do
pkg
end

def release(%Package{release: %Mix.Release{path: path} = rel} = pkg) do
def release(%Package{} = pkg) do
if pkg.webview_backend == :desktop_webview do
release_desktop_webview(pkg)
else
release_legacy_wx(pkg)
end
end

defp release_desktop_webview(%Package{release: %Mix.Release{path: path} = rel} = pkg) do
build_root = Path.join([path, "..", ".."]) |> Path.expand()
root = Path.join(build_root, "#{pkg.name}.app")
File.rm_rf(root)

contents = Path.join(root, "Contents")
bindir = Path.join(contents, "MacOS")
resources = Path.join(contents, "Resources")
beam_root = Path.join(resources, "beam")

File.mkdir_p!(bindir)
File.mkdir_p!(beam_root)

host_bin = resolve_webview_binary!(pkg)
File.cp!(host_bin, Path.join(bindir, "DesktopWebView"))
File.chmod!(Path.join(bindir, "DesktopWebView"), 0o755)

beam_app = pkg.beam_app_name || to_string(pkg.app_name || Mix.Project.config()[:app])

ini = """
[beam]
path = beam
app_name = #{beam_app}
args = start
working_dir = beam

[network]
host = 127.0.0.1
port = 0

[lifetime]
mode = reconnect
"""

File.write!(Path.join(resources, "DesktopWebView.ini"), ini)

pkg = %{pkg | priv: Map.put(pkg.priv, :bundle_executable, "DesktopWebView")}

content = eval_eex(Package.toolpath("rel/macosx/InfoPlist.strings.eex"), rel, pkg)
utf8bom = :unicode.encoding_to_bom(:utf8)

for lang <- ["en", "Base"] do
langdir = Path.join(resources, "#{lang}.lproj")
File.mkdir_p!(langdir)
File.write!(Path.join(langdir, "InfoPlist.strings"), utf8bom <> content)
end

content = eval_eex(Package.toolpath("rel/macosx/Info.plist.eex"), rel, pkg)
File.write!(Path.join(contents, "Info.plist"), content)
File.write!(Path.join(contents, "PkgInfo"), "APPL????")

File.ls!(path)
|> Enum.each(fn file ->
File.cp_r!(Path.join(path, file), Path.join(beam_root, file), fn src, dst ->
file_md5(src) != file_md5(dst)
end)
end)

icon_path = ensure_icns!(pkg, build_root)
cp!(icon_path, resources)

maybe_embed_plist_in_beam(root, content)
rewrite_app_deps(pkg, root)
finalize_macos_installer(pkg, root)
end

defp release_legacy_wx(%Package{release: %Mix.Release{path: path} = rel} = pkg) do
build_root = Path.join([path, "..", ".."]) |> Path.expand()
root = Path.join(build_root, "#{pkg.name}.app")
# Remove crust
Expand All @@ -27,6 +101,8 @@ defmodule Desktop.Deployment.Package.MacOS do

File.mkdir_p!(bindir)

pkg = %{pkg | priv: Map.put(pkg.priv, :bundle_executable, "run")}

content = eval_eex(Package.toolpath("rel/macosx/InfoPlist.strings.eex"), rel, pkg)
utf8bom = :unicode.encoding_to_bom(:utf8)

Expand All @@ -50,52 +126,52 @@ defmodule Desktop.Deployment.Package.MacOS do
end)
end)

# Creating/copying the icon
icon_path = Path.absname("rel/macosx/icons.icns")

if not File.exists?(icon_path) do
iconset = Path.join(build_root, "icons.iconset")
File.mkdir_p!(iconset)
icon_path = ensure_icns!(pkg, build_root)
cp!(icon_path, resources)
maybe_import_webview(pkg, contents)

for size <- [16, 32, 128, 256, 512] do
outfile = Path.join(iconset, "icon_#{size}x#{size}.png")
cmd!("sips", ["-z", size, size, pkg.icon, "--out", outfile])
outfile = Path.join(iconset, "icon_#{size}x#{size}@2.png")
cmd!("sips", ["-z", 2 * size, 2 * size, pkg.icon, "--out", outfile])
end
maybe_embed_plist_in_beam(root, content)
rewrite_app_deps(pkg, root)
finalize_macos_installer(pkg, root)
end

outfile = Path.join(iconset, "[email protected]")
cmd!("sips", ["-z", 1024, 1024, pkg.icon, "--out", outfile])
File.mkdir_p!(Path.dirname(icon_path))
cmd!("iconutil", ["-c", "icns", iconset, "-o", icon_path])
defp maybe_embed_plist_in_beam(root, content) do
case wildcard(root, "**/*.smp") do
[beam_smp] -> embed_plist_template(beam_smp, content)
_ -> :ok
end
end

cp!(icon_path, resources)
maybe_import_webview(pkg, contents)

# Maybe embedding Info.plist into the beam.smp
with [beam_smp] <- wildcard(root, "**/*.smp") do
oldbin = File.read!(beam_smp)
defp embed_plist_template(beam_smp, content) do
oldbin = File.read!(beam_smp)

with [match] <-
Regex.run(
~r/<\!--PLIST_TEMPLATE_START_64f5fc2af15ab6092d25ede0fdc039e0789aa6e9.+PLIST_TEMPLATE_END_64f5fc2af15ab6092d25ede0fdc039e0789aa6e9-->/s,
oldbin
) do
case Regex.run(
~r/<\!--PLIST_TEMPLATE_START_64f5fc2af15ab6092d25ede0fdc039e0789aa6e9.+PLIST_TEMPLATE_END_64f5fc2af15ab6092d25ede0fdc039e0789aa6e9-->/s,
oldbin
) do
[match] ->
size = byte_size(match)
[_all, replacement] = Regex.run(~r/<plist[^>]*>(.+)<\/plist>/s, content)
replacement = String.pad_trailing(replacement, size, " ")
bin = String.replace(oldbin, match, replacement)
IO.puts("Embedding Info.plist into beam.smp[#{byte_size(oldbin)} -> #{byte_size(bin)}]")
File.write!(beam_smp, bin)
cmd!("codesign", ["-s", "-", beam_smp])
end
end

for bin <- find_binaries(root) do
rewrite_deps(bin, &maybe_rewrite_dep_to_approot(pkg, bin, &1, root))
_ ->
:ok
end
end

defp rewrite_app_deps(pkg, root) do
Enum.each(find_binaries(root), &rewrite_binary_deps(&1, pkg, root))
end

defp rewrite_binary_deps(bin, pkg, root) do
rewrite_deps(bin, &maybe_rewrite_dep_to_approot(pkg, bin, &1, root))
end

defp finalize_macos_installer(pkg, root) do
developer_id = find_developer_id()

if developer_id != nil do
Expand All @@ -106,12 +182,64 @@ defmodule Desktop.Deployment.Package.MacOS do

dmg = make_dmg(pkg)
maybe_make_pkg(pkg)
if developer_id != nil, do: package_sign(developer_id, dmg)

if developer_id != nil do
package_sign(developer_id, dmg)
%{pkg | priv: Map.put(pkg.priv, :installer_name, dmg)}
end

defp ensure_icns!(%Package{} = pkg, build_root) do
icon_path = Path.absname("rel/macosx/icons.icns")

if not File.exists?(icon_path) do
iconset = Path.join(build_root, "icons.iconset")
File.mkdir_p!(iconset)

for size <- [16, 32, 128, 256, 512] do
outfile = Path.join(iconset, "icon_#{size}x#{size}.png")
cmd!("sips", ["-z", size, size, pkg.icon, "--out", outfile])
outfile = Path.join(iconset, "icon_#{size}x#{size}@2.png")
cmd!("sips", ["-z", 2 * size, 2 * size, pkg.icon, "--out", outfile])
end

outfile = Path.join(iconset, "[email protected]")
cmd!("sips", ["-z", 1024, 1024, pkg.icon, "--out", outfile])
File.mkdir_p!(Path.dirname(icon_path))
cmd!("iconutil", ["-c", "icns", iconset, "-o", icon_path])
end

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

defp resolve_webview_binary!(%Package{} = pkg) do
candidates =
[
pkg.webview_binary,
System.get_env("DESKTOP_WEBVIEW_BINARY"),
desktop_webview_priv_binary(),
Path.expand("deps/desktop_webview/priv/native/macos/DesktopWebView"),
Path.expand("../webview/priv/native/macos/DesktopWebView"),
Path.expand("../../webview/priv/native/macos/DesktopWebView")
]
|> Enum.filter(&(is_binary(&1) and &1 != ""))

case Enum.find(candidates, &File.exists?/1) do
nil ->
raise """
DesktopWebView host binary not found for macOS packaging.

Set DESKTOP_WEBVIEW_BINARY, package.webview_binary, or build/install
:desktop_webview so priv/native/macos/DesktopWebView exists.
"""

path ->
path
end
end

defp desktop_webview_priv_binary do
Application.app_dir(:desktop_webview, "priv/native/macos/DesktopWebView")
rescue
_ -> nil
end

def package_sign(developer_id, dmg) do
Expand Down
8 changes: 6 additions & 2 deletions rel/macosx/Info.plist.eex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<key>CFBundleIdentifier</key>
<string><%= @package.identifier %></string>
<key>CFBundleExecutable</key>
<string>run</string>
<string><%= Map.get(@package.priv, :bundle_executable, "DesktopWebView") %></string>
<key>CFBundleIconFile</key>
<string>icons.icns</string>
<key>CFBundlePackageType</key>
Expand All @@ -21,9 +21,13 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>LSMinimumSystemVersion</key>
<string>10.13</string>
<string>13.0</string>
<key>NSUserNotificationAlertStyle</key>
<string>alert</string>
<key>NSMicrophoneUsageDescription</key>
<string><%= @package.microphone_usage %></string>
<key>NSCameraUsageDescription</key>
<string><%= @package.camera_usage %></string>
<key>CFBundleURLTypes</key>
<array>
<dict>
Expand Down
Loading