Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(shared-libs): correctly resolve libwebp libraries #88

Merged
merged 4 commits into from
Nov 16, 2023

Conversation

jimeh
Copy link
Owner

@jimeh jimeh commented Oct 11, 2023

This is a near complete rewrite of the shared library embedder/bundler code. It
now correctly resolves shared libraries that link to other shared library files
in the same package via use of @path, which the libwebp package does
extensively between libwebpdemux.2.dylib, libwebp.7.dylib and
libsharpyuv.0.dylib.

These relative shared library links were not understood at all by the old
library bundler code.

The new replacement here fully understands and resolves all @<something>
placeholders in links to shared libraries. It operates in a two-step process
where it first walks down the three of all shared libraries that Emacs links to,
and all that they link to, etc. building a copy and relink plan.

In a second step it then executes the copy plan, followed by the relinking plan.

The result is a fully self-contained Emacs.app binary, which has no dependencies
on Homebrew or anything installed via Homebrew. Builds have been tested on a
fresh install of macOS Sonoma VM, with nothing but Xcode Command Line Tools
installed. Everything from native compilation to webp, svg, png rendering, and
more worked as expected.

@jimeh jimeh force-pushed the refactor-shared-library-bundler-logic branch from 0ce6900 to f1d5bcf Compare November 14, 2023 20:22
@jimeh jimeh changed the title wip(shared-libs): refactor bundler to address libs using rpath internally fix(shared-libs): correctly resolve libwebp libraries Nov 14, 2023
@jimeh jimeh marked this pull request as ready for review November 14, 2023 20:23
@garyo
Copy link

garyo commented Nov 15, 2023

Just did a local build using this branch. Not quite there on my arm64 Mac; maybe the libgnutls dylib hasn't been tweaked to find the local libintl. I haven't poked into the details yet.

% /Applications/Emacs.app/Contents/MacOS/Emacs 
dyld[32900]: Library not loaded: @loader_path/../../../../opt/gettext/lib/libintl.8.dylib
  Referenced from: <A95699CE-2BA7-3EA0-A074-A47CAB443C4D> /Applications/Emacs.app/Contents/Frameworks/libgnutls.30.dylib
  Reason: tried: '/Applications/Emacs.app/Contents/Frameworks/../../../../opt/gettext/lib/libintl.8.dylib' (no such file), '/usr/local/lib/libintl.8.dylib' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64')), '/usr/lib/libintl.8.dylib' (no such file, not in dyld cache)
[1]    32900 abort      /Applications/Emacs.app/Contents/MacOS/Emacs
% file /Applications/Emacs.app/Contents/MacOS/Emacs
/Applications/Emacs.app/Contents/MacOS/Emacs: Mach-O 64-bit executable arm64
% fd libintl /Applications/Emacs.app                     
/Applications/Emacs.app/Contents/Frameworks/libintl.8.dylib
% file $(fd libintl /Applications/Emacs.app)
/Applications/Emacs.app/Contents/Frameworks/libintl.8.dylib: Mach-O 64-bit dynamically linked shared library arm64
% otool -L /Applications/Emacs.app/Contents/Frameworks/libgnutls.30.dylib
/Applications/Emacs.app/Contents/Frameworks/libgnutls.30.dylib:
	@rpath/libgnutls.30.dylib (compatibility version 67.0.0, current version 67.0.0)
	/System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 1.0.0, current version 60420.101.2)
	/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1971.0.0)
	/usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.11)
	@loader_path/../../../../opt/gettext/lib/libintl.8.dylib (compatibility version 12.0.0, current version 12.0.0)
	@loader_path/../../../../opt/p11-kit/lib/libp11-kit.0.dylib (compatibility version 4.0.0, current version 4.0.0)
	@loader_path/../../../../opt/libidn2/lib/libidn2.0.dylib (compatibility version 4.0.0, current version 4.8.0)
	@loader_path/../../../../opt/libunistring/lib/libunistring.5.dylib (compatibility version 6.0.0, current version 6.0.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.100.3)
	@loader_path/../../../../opt/libtasn1/lib/libtasn1.6.dylib (compatibility version 13.0.0, current version 13.3.0)
	@loader_path/../../../../opt/nettle/lib/libnettle.8.dylib (compatibility version 8.0.0, current version 8.8.0)
	@loader_path/../../../../opt/nettle/lib/libhogweed.6.dylib (compatibility version 6.0.0, current version 6.8.0)
	@loader_path/../../../../opt/gmp/lib/libgmp.10.dylib (compatibility version 15.0.0, current version 15.1.0)

⇒ update: it's not a libgnutls problem; my older working build has basically the same otool -L output for that lib. Maybe that my older, working build is still depending on homebrew libs but the newer one doesn't.

% otool -L /Applications/Emacs.app/Contents/MacOS/Emacs | grep intl # old build
	/opt/homebrew/opt/gettext/lib/libintl.8.dylib (compatibility version 13.0.0, current version 13.0.0)
% otool -L /tmp/Emacs.app/Contents/MacOS/Emacs | grep intl # new build
	@rpath/libintl.8.dylib (compatibility version 13.0.0, current version 13.0.0)

The new way ought to work because the Emacs exe sets @rpath to @executable_path/../Frameworks. Looking at the DYLD_PRINT_LIBRARIES output, I see that Emacs loads libintl.8.dylib directly but then also tries to load the one with ../../../ in its path, which fails. Let me know if I can do any more testing.

@jimeh
Copy link
Owner Author

jimeh commented Nov 15, 2023

@garyo Ok, those @loader_path/../../.. paths are very wrong. There's clearly a bug with the new dylib absolute path resolver logic which I haven't come across.

Would you mind running the build script with --log-level debug, and looking for the following output towards the end:

==> DEBUG: Changing linked dylibs in: 'Contents/Frameworks/libgnutls.30.dylib'
==> DEBUG: -- Relinking '/opt/homebrew/opt/gettext/lib/libintl.8.dylib' as: '@rpath/libintl.8.dylib'
==> DEBUG: -- Relinking '/opt/homebrew/opt/p11-kit/lib/libp11-kit.0.dylib' as: '@rpath/libp11-kit.0.dylib'
==> DEBUG: -- Relinking '/opt/homebrew/opt/libidn2/lib/libidn2.0.dylib' as: '@rpath/libidn2.0.dylib'
==> DEBUG: -- Relinking '/opt/homebrew/opt/libunistring/lib/libunistring.5.dylib' as: '@rpath/libunistring.5.dylib'
==> DEBUG: -- Relinking '/opt/homebrew/opt/libtasn1/lib/libtasn1.6.dylib' as: '@rpath/libtasn1.6.dylib'
==> DEBUG: -- Relinking '/opt/homebrew/opt/nettle/lib/libnettle.8.dylib' as: '@rpath/libnettle.8.dylib'
==> DEBUG: -- Relinking '/opt/homebrew/opt/nettle/lib/libhogweed.6.dylib' as: '@rpath/libhogweed.6.dylib'
==> DEBUG: -- Relinking '/opt/homebrew/opt/gmp/lib/libgmp.10.dylib' as: '@rpath/libgmp.10.dylib'

(if you want to speed things up, you can also use --no-archive and --git-sha to fix it to a specific commit sha, and just trash the output in the builds directory each time, as the build script will find the already compiled Emacs.app within sources/**/nextstep and just re-use it. This makes it relatively easy to quickly test changes to the shared library bundler.)

For reference this what the links look like in my local build for libgnutls.30.dylib:

otool -L /Applications/Emacs.app/Contents/Frameworks/libgnutls.30.dylib                                                                                                                                                                                                                                                                                                                                            [14:13:41]
/Applications/Emacs.app/Contents/Frameworks/libgnutls.30.dylib:
        @rpath/libgnutls.30.dylib (compatibility version 67.0.0, current version 67.0.0)
        /System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 1.0.0, current version 61040.1.3)
        /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 2048.1.255)
        /usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.12)
        @rpath/libintl.8.dylib (compatibility version 13.0.0, current version 13.0.0)
        @rpath/libp11-kit.0.dylib (compatibility version 4.0.0, current version 4.0.0)
        @rpath/libidn2.0.dylib (compatibility version 4.0.0, current version 4.8.0)
        @rpath/libunistring.5.dylib (compatibility version 6.0.0, current version 6.0.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1336.0.0)
        @rpath/libtasn1.6.dylib (compatibility version 13.0.0, current version 13.3.0)
        @rpath/libnettle.8.dylib (compatibility version 8.0.0, current version 8.8.0)
        @rpath/libhogweed.6.dylib (compatibility version 6.0.0, current version 6.8.0)
        @rpath/libgmp.10.dylib (compatibility version 15.0.0, current version 15.1.0)

@jimeh
Copy link
Owner Author

jimeh commented Nov 15, 2023

@garyo Also, what version of gnutls do you have installed via Homebrew? brew info gnutls should show it. If you have an older version, that might explain it, but also would mean I can install that specific version and try to reproduce the issue :)

@garyo
Copy link

garyo commented Nov 15, 2023

gnutls:

% brew info gnutls
==> gnutls: stable 3.8.1 (bottled

Debug log is here

@garyo
Copy link

garyo commented Nov 15, 2023

Perhaps this is significant -- it thinks libgnutls depends on a non-homebrew libintl and so skips it:

11558   │ ==> DEBUG: Calculating bundling instructions for: /opt/homebrew/opt/gnutls/lib/libgnutls.30.dylib
11559   │ ==> DEBUG: -- Processing shared library: /System/Library/Frameworks/Security.framework/Versions/A/Security
11560   │ ==> DEBUG: -- -- Skipping, not from lib_source: /opt/homebrew
11561   │ ==> DEBUG: -- Processing shared library: /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
11562   │ ==> DEBUG: -- -- Skipping, not from lib_source: /opt/homebrew
11563   │ ==> DEBUG: -- Processing shared library: /usr/lib/libz.1.dylib
11564   │ ==> DEBUG: -- -- Skipping, not from lib_source: /opt/homebrew
11565   │ ==> DEBUG: -- Processing shared library: @loader_path/../../../../opt/gettext/lib/libintl.8.dylib
11566   │ ==> DEBUG: -- -- Resolved to: /opt/opt/gettext/lib/libintl.8.dylib
11567   │ ==> DEBUG: -- -- Skipping, not from lib_source: /opt/homebrew

@garyo
Copy link

garyo commented Nov 15, 2023

Maybe this is helpful: why are there 4 .. in the @loader_path/../../../../opt/gettext/lib/libintl.8.dylib in libgnutls.30.dylib? You'd think that only three sets of .. are needed. But there are some symlinks that make it work.

% realpath /opt/homebrew/opt/gnutls/lib            
/opt/homebrew/Cellar/gnutls/3.8.1/lib
% realpath /opt/homebrew/opt/gnutls/lib/..
/opt/homebrew/Cellar/gnutls/3.8.1
% realpath /opt/homebrew/opt/gnutls/lib/../..
/opt/homebrew/Cellar/gnutls
% realpath /opt/homebrew/opt/gnutls/lib/../../..
/opt/homebrew/Cellar
% realpath /opt/homebrew/opt/gnutls/lib/../../../..
/opt/homebrew
# and finally:
% realpath /opt/homebrew/opt/gnutls/lib/../../../../opt/gettext/lib/libintl.8.dylib
/opt/homebrew/Cellar/gettext/0.22.3/lib/libintl.8.dylib

... so you may need some realpath calls in your path resolution?

@jimeh
Copy link
Owner Author

jimeh commented Nov 15, 2023

Interesting, what do the links look like in the dylib within homebrew? Here's mine:

$ otool -L /opt/homebrew/opt/gnutls/lib/libgnutls.30.dylib                                                                                                              (8s) [14:34:53]
/opt/homebrew/opt/gnutls/lib/libgnutls.30.dylib:
        /opt/homebrew/opt/gnutls/lib/libgnutls.30.dylib (compatibility version 67.0.0, current version 67.0.0)
        /System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 1.0.0, current version 61040.1.3)
        /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 2048.1.255)
        /usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.12)
        /opt/homebrew/opt/gettext/lib/libintl.8.dylib (compatibility version 13.0.0, current version 13.0.0)
        /opt/homebrew/opt/p11-kit/lib/libp11-kit.0.dylib (compatibility version 4.0.0, current version 4.0.0)
        /opt/homebrew/opt/libidn2/lib/libidn2.0.dylib (compatibility version 4.0.0, current version 4.8.0)
        /opt/homebrew/opt/libunistring/lib/libunistring.5.dylib (compatibility version 6.0.0, current version 6.0.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1336.0.0)
        /opt/homebrew/opt/libtasn1/lib/libtasn1.6.dylib (compatibility version 13.0.0, current version 13.3.0)
        /opt/homebrew/opt/nettle/lib/libnettle.8.dylib (compatibility version 8.0.0, current version 8.8.0)
        /opt/homebrew/opt/nettle/lib/libhogweed.6.dylib (compatibility version 6.0.0, current version 6.8.0)
        /opt/homebrew/opt/gmp/lib/libgmp.10.dylib (compatibility version 15.0.0, current version 15.1.0)

And I'll have a quick stab at adding a realpath lookup now to see if that solves it for you.

@garyo
Copy link

garyo commented Nov 15, 2023

This fix works for me, though I'm no ruby programmer:

% git diff
diff --git a/build-emacs-for-macos b/build-emacs-for-macos
index a0a2a31..e350526 100755
--- a/build-emacs-for-macos
+++ b/build-emacs-for-macos
@@ -1105,7 +1105,8 @@ class LibEmbedder < AbstractEmbedder
       fatal "Could not resolve path: #{path}" if abs.nil?
     end
 
-    File.expand_path(abs)
+    result = File.expand_path(abs)
+    File.exist?(result) && File.realpath(result) || result
   end
 
   def build_bundle_plan(macho_file, copy_macho_file: false)

@garyo
Copy link

garyo commented Nov 15, 2023

To your question about my libgnutls:

% otool -L /opt/homebrew/opt/gnutls/lib/libgnutls.30.dylib
/opt/homebrew/opt/gnutls/lib/libgnutls.30.dylib:
	/opt/homebrew/opt/gnutls/lib/libgnutls.30.dylib (compatibility version 67.0.0, current version 67.0.0)
	/System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 1.0.0, current version 60420.101.2)
	/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1971.0.0)
	/usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.11)
	@loader_path/../../../../opt/gettext/lib/libintl.8.dylib (compatibility version 12.0.0, current version 12.0.0)
	@loader_path/../../../../opt/p11-kit/lib/libp11-kit.0.dylib (compatibility version 4.0.0, current version 4.0.0)
	@loader_path/../../../../opt/libidn2/lib/libidn2.0.dylib (compatibility version 4.0.0, current version 4.8.0)
	@loader_path/../../../../opt/libunistring/lib/libunistring.5.dylib (compatibility version 6.0.0, current version 6.0.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.100.3)
	@loader_path/../../../../opt/libtasn1/lib/libtasn1.6.dylib (compatibility version 13.0.0, current version 13.3.0)
	@loader_path/../../../../opt/nettle/lib/libnettle.8.dylib (compatibility version 8.0.0, current version 8.8.0)
	@loader_path/../../../../opt/nettle/lib/libhogweed.6.dylib (compatibility version 6.0.0, current version 6.8.0)
	@loader_path/../../../../opt/gmp/lib/libgmp.10.dylib (compatibility version 15.0.0, current version 15.1.0)

I wonder why mine is different?

@jimeh
Copy link
Owner Author

jimeh commented Nov 15, 2023

@garyo I've pushed a fix that should hopefully work for you. Just saw your patch too, mine is similar, but should yield the same result more or less.

If that works, I'd then be curious to see what happens with the links in libgnutls.30.dylib if you reinstall it with brew reinstall gnutls.

@garyo
Copy link

garyo commented Nov 15, 2023

Works for me! Also just to confirm, the resulting emacs has no dependencies from homebrew when I check with otool -L. Looking good!

This is a near complete rewrite of the shared library embedder/bundler
code. It now correctly resolves shared libraries that link to other
shared library files in the same package via use of `@path`, which the
libwebp package does extensively between `libwebpdemux.2.dylib`,
`libwebp.7.dylib` and `libsharpyuv.0.dylib`.

These relative shared library links were not understood at all by the
old library bundler code.

The new replacement here fully understands and resolves all
`@<something>` placeholders in links to shared libraries. It operates in
a two-step process where it first walks down the three of all shared
libraries that Emacs links to, and all that they link to, etc. building
a copy and relink plan.

In a second step it then executes the copy plan, followed by the
relinking plan.

The result is a fully self-contained Emacs.app binary, which has no
dependencies on Homebrew or anything installed via Homebrew. Builds have
been tested on a fresh install of macOS Sonoma VM, with nothing but
Xcode Command Line Tools installed. Everything from native compilation
to webp, svg, png rendering, and more worked as expected.
This should resolve issues on Apple Silicon machines where macOS refuses
to run applications without any signatures. On Intel machines it seems
to make not difference.

If you want to skip the self-signing step, use the `--no-self-sign`
flag.
@jimeh jimeh force-pushed the refactor-shared-library-bundler-logic branch from 05ffe6d to a534760 Compare November 16, 2023 12:58
@jimeh jimeh linked an issue Nov 16, 2023 that may be closed by this pull request
@jimeh jimeh merged commit 8c9aba9 into master Nov 16, 2023
2 of 3 checks passed
@jimeh jimeh deleted the refactor-shared-library-bundler-logic branch November 16, 2023 13:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Maybe need add libwebp.7.dylib to Frameworks folder?
2 participants