erlang / otp

Erlang/OTP
http://erlang.org
Apache License 2.0
11.36k stars 2.95k forks source link

Incorrect .app bundle location on macOS #6070

Open wojtekmach opened 2 years ago

wojtekmach commented 2 years ago

Describe the bug When packaging up a Erlang in an .app bundle on macOS, the system runtime incorrectly reports the bundle location and cannot retrieve any of the bundle keys.

Here is at least one place in OTP that could benefit from improvements in this area: https://github.com/erlang/otp/blob/OTP-25.0.1/lib/wx/c_src/wxe_ps_init.c#L38:L41

It might be a problem in OTP but it is also very possible it's just a matter of proper packaging.

To Reproduce

  1. Apply this patch to get additional debugging information
diff --git a/lib/wx/c_src/wxe_ps_init.c b/lib/wx/c_src/wxe_ps_init.c
index 473354a671..56a0d13ec9 100644
--- a/lib/wx/c_src/wxe_ps_init.c
+++ b/lib/wx/c_src/wxe_ps_init.c
@@ -58,6 +58,8 @@ void * wxe_ps_init2() {

    // Setup and enable gui
    pool = [[NSAutoreleasePool alloc] init];
+   NSLog(@"bundleURL=%@", [[NSBundle mainBundle] bundleURL]);
+   NSLog(@"bundleIdentifier=%@", [[NSBundle mainBundle] bundleIdentifier]);

    if( !is_packaged_app() ) {
       // Undocumented function (but no documented way of doing this exists)
  1. Run this script
#!/bin/sh
set -e pipefail
export MAKEFLAGS=-j8
export ERL_TOP=$PWD

./otp_build setup -a
./otp_build boot -a
./otp_build release -a

rm -rf MyApp.app
mkdir -p MyApp.app/Contents/Resources
cp -R release/`$ERL_TOP/make/autoconf/config.guess` MyApp.app/Contents/Resources/erlang
(cd MyApp.app/Contents/Resources/erlang && ./Install -sasl $PWD)

cat << EOF > MyApp.app/Contents/Info.plist
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>CFBundleName</key>
  <string>MyApp</string>
  <key>CFBundleVersion</key>
  <string>1.0.0</string>
  <key>CFBundleIdentifier</key>
  <string>myapp</string>
</dict>
</plist>
EOF

mkdir -p MyApp.app/Contents/MacOS
cat << EOF > MyApp.app/Contents/MacOS/MyApp
#!/bin/sh
set -e pipefail
\$(dirname \$(realpath \$0))/../Resources/erlang/bin/erl \
    -noshell -eval 'wx:new(),timer:sleep(5000),halt().'
EOF
chmod +x MyApp.app/Contents/MacOS/MyApp
  1. Run the app
$ open MyApp.app --stdout $TTY --stderr $TTY

For me, it prints:

2022-06-09 23:47:08.360 beam.smp[88845:3652680] bundleURL=file:///Users/wojtek/src/otp/MyApp.app/Contents/Resources/erlang/erts-13.0.1/bin/
2022-06-09 23:47:08.360 beam.smp[88845:3652680] bundleIdentifier=(null)

Expected behavior

Report the proper bundle information:

bundleURL=file:///Users/wojtek/src/otp/MyApp.app
bundleIdentifier=myapp

Affected versions Most likely all

Additional context

I have determined that updating erts/etc/darwin/Info.plist, e.g. setting CFBundleIdentifier will make it so that identifier will be reported by macOS runtime. That is not helping much as ideally we wouldn't need to touch that file. In fact in https://github.com/erlang/otp/pull/5393 we specifically removed some CFBundle* fields exactly so that people that do packaging could set everything themselves.

wojtekmach commented 2 years ago

I was able to make it work. It requires two things:

  1. Remove erts/etc/darwin/Info.plist
  2. Put erlexec and beam.smp in the same directory as the app's executable. A symlink is enough.

Here is roughly how the bundle looks like:

MyApp.app/
  Contents/
    Info.plist
    MacOS/
      MyApp    # bash script
      erlexec  # symlink to ../Resources/otp/erts-VSN/erlexec
      beam.smp # symlink to ../Resources/otp/erts-VSN/beam.smp
    Resources/
      otp/

I'd love to find a solution that doesn't require copying or symlinking files to the Contents/MacOS directory and instead set everything in the launcher executable. Any help with that would be very appreciated.

For anyone interested, here is a step-by-step guide how to reproduce my results in an OTP git checkout.

Step 1: add some debugging

diff --git a/lib/wx/c_src/wxe_ps_init.c b/lib/wx/c_src/wxe_ps_init.c
index 473354a671..56a0d13ec9 100644
--- a/lib/wx/c_src/wxe_ps_init.c
+++ b/lib/wx/c_src/wxe_ps_init.c
@@ -58,6 +58,8 @@ void * wxe_ps_init2() {

    // Setup and enable gui
    pool = [[NSAutoreleasePool alloc] init];
+   NSLog(@"bundleURL=%@", [[NSBundle mainBundle] bundleURL]);
+   NSLog(@"bundleIdentifier=%@", [[NSBundle mainBundle] bundleIdentifier]);

    if( !is_packaged_app() ) {
       // Undocumented function (but no documented way of doing this exists)

Step 2: Remove Info.plist

diff --git a/erts/emulator/Makefile.in b/erts/emulator/Makefile.in
index 979f76c4c6..c73c3bc1c7 100644
--- a/erts/emulator/Makefile.in
+++ b/erts/emulator/Makefile.in
@@ -242,9 +242,6 @@ VOID_EMULATOR =
 endif

 OPSYS=@OPSYS@
-ifeq ($(OPSYS),darwin)
-LDFLAGS += -sectcreate __TEXT __info_plist "$(ERL_TOP)/erts/etc/darwin/Info.plist"
-endif

 sol2CFLAGS=
 linuxCFLAGS=
diff --git a/erts/etc/darwin/Info.plist b/erts/etc/darwin/Info.plist
deleted file mode 100644
index 3896524588..0000000000
--- a/erts/etc/darwin/Info.plist
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-       <key>NSAppTransportSecurity</key>
-       <dict>
-               <key>NSExceptionDomains</key>
-               <dict>
-                       <key>localhost</key>
-                       <dict>
-                               <key>NSExceptionAllowsInsecureHTTPLoads</key>
-                               <true/>
-                               <key>NSIncludesSubdomains</key>
-                               <true/>
-                       </dict>
-               </dict>
-       </dict>
-</dict>
-</plist>

Step 3: Run this script to build the app

#!/bin/sh
set -e pipefail
export MAKEFLAGS=-j8
export ERL_TOP=$PWD
erts_version="13.0.2"

# ./otp_build setup -a
# ./otp_build boot -a
# ./otp_build release -a

rm -rf MyApp.app
mkdir -p MyApp.app/Contents/Resources
cp -R release/`$ERL_TOP/make/autoconf/config.guess` MyApp.app/Contents/Resources/otp
(cd MyApp.app/Contents/Resources/otp && ./Install -sasl $PWD)

cat << EOF > MyApp.app/Contents/Info.plist
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>CFBundleName</key>
  <string>MyApp</string>
  <key>CFBundleVersion</key>
  <string>1.0.0</string>
  <key>CFBundleIdentifier</key>
  <string>myapp</string>
</dict>
</plist>
EOF

mkdir -p MyApp.app/Contents/MacOS
(cd MyApp.app/Contents/MacOS && ln -s ../Resources/otp/erts-${erts_version}/bin/{erlexec,beam.smp} .)

cat << EOF > MyApp.app/Contents/MacOS/MyApp
#!/bin/sh
set -e pipefail
cwd=\$(dirname \$0)
export EMU=beam
export BINDIR=\$cwd
export ROOTDIR=\$cwd/../Resources/otp
\$cwd/erlexec -noshell -eval 'wx:new(), timer:sleep(5000), halt().'
EOF
chmod +x MyApp.app/Contents/MacOS/MyApp

Step 4: Run the app

$ open --stdout $TTY --stderr $TTY MyApp.app

For me, it now prints:

2022-06-28 12:34:59.511 beam.smp[28676:1151422] bundleURL=file:///Users/wojtek/src/otp/MyApp.app/
2022-06-28 12:34:59.511 beam.smp[28676:1151422] bundleIdentifier=myapp
wojtekmach commented 2 years ago

Even if we go with the hack from the previous comment, putting erlexec and beam.smp into particular locations in the bundle, we'd be running the VM in a separate OS process from the "main" executable (the one at MyApp.app/Contents/MacOS/MyApp) and it seems that is the crux of the issue.

Another way to solve is to statically link Erlang into the main executable and start it from there. Below is a script that does just that:

``` #!/bin/sh # Build OTP, a liberl.a, and a MyApp.app. Run this script in an OTP checkout. set -euo pipefail export MAKEFLAGS=-j`nproc` export ERL_TOP=`pwd` export ERLC_USE_SERVER=true # step 1: build OTP ./otp_build configure \ --with-ssl=`brew --prefix openssl@1.1` \ --disable-dynamic-ssl-lib \ --enable-builtin-zlib ./otp_build boot -s for i in crypto wx; do (cd lib/$i && make) done # step 2: build liberl.a find bin lib erts \ -name `echo {liberts_internal,libethread,libryu,libz,libepcre,libbeam,libei}.a | sed "s/ / -or -name /g"` \ | xargs libtool -o liberl.a # step 3: clone wojtekmach/bundleinfo, a NIF library that uses NSBundle API. if [ ! -d bundleinfo ]; then git clone https://github.com/wojtekmach/bundleinfo (cd bundleinfo && rebar3 compile) fi # step 4: build MyApp.app rm -rf MyApp.app mkdir -p MyApp.app/Contents/MacOS cat << EOF > main.cpp #include #include extern "C" { extern void erl_start(int argc, char **argv); } int main() { setenv("BINDIR", "${ERL_TOP}", 0); const char *args[] = { "main", "--", "-root", "${ERL_TOP}", "-bindir", "${ERL_TOP}/bin", "-noshell", "-pa", "${ERL_TOP}/bundleinfo/_build/default/lib/bundleinfo/ebin", "-eval", "ok = crypto:start().", "-eval", "wxFrame:show(wxFrame:new(wx:new(), -1, \"Demo\")).", "-eval", "bundleinfo:info().", "-eval", "timer:sleep(3000),halt().", }; erl_start(sizeof(args) / sizeof(args[0]), (char **)args); } EOF cat << EOF > MyApp.app/Contents/Info.plist CFBundleName MyApp CFBundleVersion 1.0.0 CFBundleIdentifier myapp EOF clang++ -framework Foundation -ltermcap -L. -lerl \ -o MyApp.app/Contents/MacOS/MyApp main.cpp cat << EOF Done! Open the app with: open --stderr \$TTY --stdout \$TTY MyApp.app EOF ```

Which would print:

bundleURL=file:///Users/wojtek/src/otp/MyApp.app/
infoDictionary={
    CFBundleIdentifier = myapp;
    CFBundleName = MyApp;
    CFBundleNumericVersion = 16809984;
    CFBundleVersion = "1.0.0";
}

which is exactly what I wanted.

Worth mentioning this is a very similar approach to deploying OTP on iOS where we need to statically link everything. On a Mac, though, it is totally fine to dynamically link things.

I believe this way of deploying OTP in macOS apps is pretty promising.

At the moment one obvious problem with this is it relies on internals, namely this function:

extern void erl_start(int argc, char **argv);

and various libX.a files.

If this approach is reasonable, it would be really convenient if otp_build could output something like this liberl.a (and/or .so) and an associated header.

Looking forward to any feedback!

PS Big thanks to @dominicletz for working on deploying OTP to iOS and recently helping us using the statically linking approach on Mac.