fix: mac bundle now proper arm64
This commit is contained in:
@@ -12,7 +12,7 @@ defmodule BDS.MacBundle do
|
|||||||
|
|
||||||
BDS2.app/Contents/
|
BDS2.app/Contents/
|
||||||
Info.plist PkgInfo
|
Info.plist PkgInfo
|
||||||
MacOS/bds2 # launcher script (execs the release)
|
MacOS/bds2 # native arm64 launcher (execs the release)
|
||||||
Resources/AppIcon.icns
|
Resources/AppIcon.icns
|
||||||
Resources/rel/ # the mix release (ERTS bundled)
|
Resources/rel/ # the mix release (ERTS bundled)
|
||||||
Frameworks/ # relocated wx + transitive dylibs
|
Frameworks/ # relocated wx + transitive dylibs
|
||||||
@@ -45,18 +45,6 @@ defmodule BDS.MacBundle do
|
|||||||
EEx.eval_file(template_path("Info.plist.eex"), assigns)
|
EEx.eval_file(template_path("Info.plist.eex"), assigns)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "Render the `Contents/MacOS` launcher shell script."
|
|
||||||
@spec launcher(keyword()) :: String.t()
|
|
||||||
def launcher(opts \\ []) do
|
|
||||||
assigns = [
|
|
||||||
identifier: Keyword.get(opts, :identifier, @identifier),
|
|
||||||
name: Keyword.get(opts, :name, @display_name),
|
|
||||||
release_name: Keyword.get(opts, :release_name, @release_name)
|
|
||||||
]
|
|
||||||
|
|
||||||
EEx.eval_file(template_path("launcher.sh.eex"), assigns)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Assemble the `.app` from a built release directory.
|
Assemble the `.app` from a built release directory.
|
||||||
|
|
||||||
@@ -96,9 +84,19 @@ defmodule BDS.MacBundle do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# The main executable must be a real arm64 Mach-O, not a shell script — a
|
||||||
|
# script has no Mach-O slices, so LaunchServices advertises x86_64 and offers
|
||||||
|
# Rosetta. Compile the native stub (launcher.c) in its place.
|
||||||
defp write_launcher(path) do
|
defp write_launcher(path) do
|
||||||
with :ok <- File.write(path, launcher()) do
|
src = template_path("launcher.c")
|
||||||
File.chmod(path, 0o755)
|
|
||||||
|
case System.cmd(
|
||||||
|
"cc",
|
||||||
|
["-arch", "arm64", "-O2", ~s(-DBDS_RELEASE_NAME="#{@release_name}"), "-o", path, src],
|
||||||
|
stderr_to_stdout: true
|
||||||
|
) do
|
||||||
|
{_, 0} -> File.chmod(path, 0o755)
|
||||||
|
{out, code} -> {:error, "launcher compile failed (#{code}): #{out}"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -135,7 +133,8 @@ defmodule BDS.MacBundle do
|
|||||||
may reference a Homebrew/local (`/opt/homebrew`, `/usr/local`) path. This is a
|
may reference a Homebrew/local (`/opt/homebrew`, `/usr/local`) path. This is a
|
||||||
hard requirement — the app must run on a Mac without Homebrew installed.
|
hard requirement — the app must run on a Mac without Homebrew installed.
|
||||||
"""
|
"""
|
||||||
@spec verify_standalone(String.t()) :: :ok | {:error, {:external_refs, [{String.t(), String.t()}]}}
|
@spec verify_standalone(String.t()) ::
|
||||||
|
:ok | {:error, {:external_refs, [{String.t(), String.t()}]}}
|
||||||
def verify_standalone(app) do
|
def verify_standalone(app) do
|
||||||
offenders =
|
offenders =
|
||||||
app
|
app
|
||||||
|
|||||||
@@ -26,6 +26,11 @@
|
|||||||
<string><%= min_system %></string>
|
<string><%= min_system %></string>
|
||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<!-- Main executable is a shell script (no Mach-O slices), so LaunchServices
|
||||||
|
otherwise advertises x86_64 and offers Rosetta. All embedded binaries are
|
||||||
|
arm64; force native execution so the OS never translates the app. -->
|
||||||
|
<key>LSRequiresNativeExecution</key>
|
||||||
|
<true/>
|
||||||
<key>LSApplicationCategoryType</key>
|
<key>LSApplicationCategoryType</key>
|
||||||
<string>public.app-category.developer-tools</string>
|
<string>public.app-category.developer-tools</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
|
|||||||
51
priv/desktop/macos/launcher.c
Normal file
51
priv/desktop/macos/launcher.c
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// Native launcher for the BDS2 .app — compiled to a pure arm64 Mach-O by
|
||||||
|
// mix bds.bundle.macos. A shell-script main executable has no Mach-O slices, so
|
||||||
|
// LaunchServices advertises x86_64 and offers Rosetta; a real arm64 binary makes
|
||||||
|
// the bundle unambiguously Apple Silicon.
|
||||||
|
//
|
||||||
|
// Mirrors launcher.sh: resolve <Contents>, prepend Frameworks to the dyld
|
||||||
|
// fallback path, then exec <Contents>/Resources/rel/bin/<release> start.
|
||||||
|
#include <limits.h>
|
||||||
|
#include <libgen.h>
|
||||||
|
#include <mach-o/dyld.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifndef BDS_RELEASE_NAME
|
||||||
|
#define BDS_RELEASE_NAME "bds"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
char exec_path[PATH_MAX];
|
||||||
|
uint32_t size = sizeof(exec_path);
|
||||||
|
if (_NSGetExecutablePath(exec_path, &size) != 0) return 1;
|
||||||
|
|
||||||
|
char resolved[PATH_MAX];
|
||||||
|
if (realpath(exec_path, resolved) == NULL) return 1;
|
||||||
|
|
||||||
|
// resolved = <Contents>/MacOS/bds2 ; climb two dirs to <Contents>.
|
||||||
|
char macos[PATH_MAX];
|
||||||
|
strncpy(macos, dirname(resolved), sizeof(macos) - 1);
|
||||||
|
macos[sizeof(macos) - 1] = '\0';
|
||||||
|
char *contents = dirname(macos);
|
||||||
|
|
||||||
|
char frameworks[PATH_MAX];
|
||||||
|
snprintf(frameworks, sizeof(frameworks), "%s/Frameworks", contents);
|
||||||
|
|
||||||
|
const char *existing = getenv("DYLD_FALLBACK_LIBRARY_PATH");
|
||||||
|
char fallback[2 * PATH_MAX];
|
||||||
|
if (existing && *existing)
|
||||||
|
snprintf(fallback, sizeof(fallback), "%s:%s", frameworks, existing);
|
||||||
|
else
|
||||||
|
snprintf(fallback, sizeof(fallback), "%s", frameworks);
|
||||||
|
setenv("DYLD_FALLBACK_LIBRARY_PATH", fallback, 1);
|
||||||
|
|
||||||
|
char bin[PATH_MAX];
|
||||||
|
snprintf(bin, sizeof(bin), "%s/Resources/rel/bin/%s", contents, BDS_RELEASE_NAME);
|
||||||
|
|
||||||
|
execl(bin, BDS_RELEASE_NAME, "start", (char *)NULL);
|
||||||
|
perror("execl"); // only reached if exec failed
|
||||||
|
return 127;
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Launcher for <%= name %> (<%= identifier %>).
|
|
||||||
# Generated by mix bds.bundle.macos — do not edit inside the .app.
|
|
||||||
#
|
|
||||||
# Resolves the bundle's own location and execs the embedded Elixir release.
|
|
||||||
# wxWidgets dylibs live in ../Frameworks and are referenced via @loader_path, so
|
|
||||||
# the app is self-contained regardless of where the BEAM executable runs from.
|
|
||||||
set -e
|
|
||||||
|
|
||||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
APP_CONTENTS="$(cd "$HERE/.." && pwd)"
|
|
||||||
RELEASE_ROOT="$APP_CONTENTS/Resources/rel"
|
|
||||||
|
|
||||||
# Belt-and-suspenders fallback for dyld (primary resolution is @loader_path).
|
|
||||||
export DYLD_FALLBACK_LIBRARY_PATH="$APP_CONTENTS/Frameworks:${DYLD_FALLBACK_LIBRARY_PATH:-}"
|
|
||||||
|
|
||||||
exec "$RELEASE_ROOT/bin/<%= release_name %>" start
|
|
||||||
@@ -168,7 +168,10 @@ defmodule BDS.MacBundleTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "honors a NIF-relative loader prefix" do
|
test "honors a NIF-relative loader prefix" do
|
||||||
assert Dylibs.relink_changes(["/opt/homebrew/lib/libfoo.dylib"], "@loader_path/../../Frameworks") ==
|
assert Dylibs.relink_changes(
|
||||||
|
["/opt/homebrew/lib/libfoo.dylib"],
|
||||||
|
"@loader_path/../../Frameworks"
|
||||||
|
) ==
|
||||||
[{"/opt/homebrew/lib/libfoo.dylib", "@loader_path/../../Frameworks/libfoo.dylib"}]
|
[{"/opt/homebrew/lib/libfoo.dylib", "@loader_path/../../Frameworks/libfoo.dylib"}]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -299,6 +302,10 @@ defmodule BDS.MacBundleTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe "MacBundle.assemble/1" do
|
describe "MacBundle.assemble/1" do
|
||||||
|
# assemble/1 compiles the native arm64 launcher with `cc -arch arm64`, so it
|
||||||
|
# needs the macOS toolchain (the only platform that builds the .app anyway).
|
||||||
|
@describetag :macos_tools
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
tmp = Path.join(System.tmp_dir!(), "bds-macbundle-#{System.unique_integer([:positive])}")
|
tmp = Path.join(System.tmp_dir!(), "bds-macbundle-#{System.unique_integer([:positive])}")
|
||||||
release = Path.join(tmp, "rel")
|
release = Path.join(tmp, "rel")
|
||||||
@@ -344,14 +351,15 @@ defmodule BDS.MacBundleTest do
|
|||||||
assert File.read!(Path.join(app, "Contents/PkgInfo")) == "APPL????"
|
assert File.read!(Path.join(app, "Contents/PkgInfo")) == "APPL????"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "installs an executable launcher that execs the release", %{app: app} do
|
test "installs an executable, arm64-only Mach-O launcher", %{app: app} do
|
||||||
launcher = Path.join(app, "Contents/MacOS/bds2")
|
launcher = Path.join(app, "Contents/MacOS/bds2")
|
||||||
assert File.exists?(launcher)
|
assert File.exists?(launcher)
|
||||||
%File.Stat{mode: mode} = File.stat!(launcher)
|
%File.Stat{mode: mode} = File.stat!(launcher)
|
||||||
assert (mode &&& 0o111) != 0, "launcher must be executable"
|
assert (mode &&& 0o111) != 0, "launcher must be executable"
|
||||||
script = File.read!(launcher)
|
# Must be a native arm64 Mach-O, not a script — a script has no Mach-O
|
||||||
assert script =~ "Resources/rel"
|
# slices so LaunchServices advertises x86_64 and offers Rosetta.
|
||||||
assert script =~ "bin/bds"
|
{archs, 0} = System.cmd("lipo", ["-archs", launcher])
|
||||||
|
assert String.trim(archs) == "arm64"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "copies the release tree and the icon into Resources", %{app: app} do
|
test "copies the release tree and the icon into Resources", %{app: app} do
|
||||||
|
|||||||
Reference in New Issue
Block a user