Embedding dylib in a macos bundle

I am building a macos bundle that links using a dylib that is included in my project (not built).

add_executable(${target} MACOSX_BUNDLE main.cpp)
add_library(google_angle_EGL SHARED IMPORTED)
set_target_properties(google_angle_EGL PROPERTIES
      IMPORTED_LOCATION ${GOOGLE_ANGLE_DIR}/macOS/libEGL.dylib
      FRAMEWORK         TRUE
      MACOSX_FRAMEWORK_BUNDLE_VERSION "1.0.0"
      MACOSX_FRAMEWORK_IDENTIFIER "com.google.angle.EGL"
      MACOSX_FRAMEWORK_SHORT_VERSION_STRING "1.0.0"
      )
target_link_libraries(${target} PRIVATE google_angle_EGL)

When the bundle gets generated, the imported library is not included in the “.app” although it is declared as a framework (I followed the instructions found in Professional CMake (section 24.3) for how to included a framework).

Any idea why this is not working? Is it because the library is imported instead of compiled? What can I do to embed the library in the .app?

The part of Professional CMake that you referred to is indeed only for frameworks built as part of your project. The FRAMEWORK target property only matters for targets built by your project too (you can’t rely on it being set for imported targets that are frameworks).

I don’t know off-hand whether the XCODE_EMBED_FRAMEWORKS target property handles imported targets. You’d probably have to look through the CMake source code to determine whether that is expected to work or not.

After much searching and banging my head against the wall for days, I am still at a loss. I have looked into BundleUtilities and I have made progress, but still having issues.

What I have now is a cmake script to invoke BundleUtilities like this:

separate_arguments(libs UNIX_COMMAND ${ARG_LIBS})
include(BundleUtilities)
get_bundle_and_executable(${ARG_EXECUTABLE} bundle executable valid)
if(valid)
  set(FRAMEWORK_PATH "${bundle}/Contents/Frameworks")
  foreach (lib IN LISTS libs)
    cmake_path(GET lib FILENAME filename)
    set(bundle_lib "${FRAMEWORK_PATH}/${filename}")
#    file(COPY "${lib}" DESTINATION "${FRAMEWORK_PATH}")
    copy_resolved_item_into_bundle("${lib}" "${bundle_lib}")
    list(APPEND bundle_libs "${bundle_lib}")
  endforeach ()
  fixup_bundle(${bundle} "${bundle_libs}" "")
endif()

The script gets invoked like this:

  add_custom_target(post_process
      COMMAND ${CMAKE_COMMAND} -DARG_EXECUTABLE="$<TARGET_FILE:${target}>" -DARG_LIBS="${google_angle_libs}" -P "${CMAKE_SOURCE_DIR}/cmake/MacOSCopyDynamicLibraries.cmake"
      DEPENDS ${target})

This generates the bundle (.app) with the proper format with the 2 libraries under test_raylib.app/Contents/Framework

If I comment out the fixup_bundle line, the executable (test_raylib.app/Contents/MacOS/test_raylib) is like this:

> otool -L test_raylib
test_raylib:
... truncated
	./libEGL.dylib (compatibility version 0.0.0, current version 0.0.0)
	./libGLESv2.dylib (compatibility version 0.0.0, current version 0.0.0)
... truncated

And of course that does not work, because it cannot find the libraries:

Library not loaded: */libEGL.dylib
Referenced from: <7E771F4F-2129-32E9-8EB6-1EABC34E6ECA> /Volumes/VOLUME/*/test_raylib.app/Contents/MacOS/test_raylib
Reason: tried: '*/libEGL.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS*/libEGL.dylib' (no such file), '*/libEGL.dylib' (no such file), '/usr/local/lib/libEGL.dylib' (no such file), '/usr/lib/libEGL.dylib' (no such file, not in dyld cache), '//libEGL.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS//libEGL.dylib' (no such file), '//libEGL.dylib' (no such file), '/usr/local/lib/libEGL.dylib' (no such file), '/usr/lib/libEGL.dylib' (no such file, not in dyld cache)
(terminated at launch; ignore backtrace)

If I restore the fixup_bundle line, then the executable seems to be properly “fixed” as in, now the path is correct:

> otool -L test_raylib
test_raylib:
... truncated
	@executable_path/../Frameworks/libEGL.dylib (compatibility version 0.0.0, current version 0.0.0)
	@executable_path/../Frameworks/libGLESv2.dylib (compatibility version 0.0.0, current version 0.0.0)
... truncated

But when I run it fails again, this time with a different message:

Exception Type:        EXC_BAD_ACCESS (SIGKILL (Code Signature Invalid))

Note that I never run any kind of code signature at all.

Any idea what I can do to fix this issue. I didn’t think it was going to be this hard to add an external dylib to my project!

You might be able to adapt the .apple scripts here to do what you need. There are some comments around that should help guide as the CMake is probably not 100% useful as-is for arbitrary projects.

After more trial and error, I was finally able to come up with a satisfactory solution.

First of all, I could never make BundleUtilities/fixup_bundle work. I have a feeling that fixup_bundle ended up removing some rpath in one of the libraries which made it fail to load.

So this is what I came up with. I have no idea whether it is the right approach, but it does work for my use case:

Following Apple’s documentation, I manually updated the 2 libraries that I am using, something like this:

> install_name_tool -id "@rpath/libEGL.dylib" libEGL.dylib
> codesign -s "-" -fv libEGL.dylib

Then I simply link with the modified libraries:

  set(GOOGLE_ANGLE_DIR "${CMAKE_CURRENT_LIST_DIR}/external/google/angle")
  list(APPEND google_angle_libs "${GOOGLE_ANGLE_DIR}/macOS/libEGL.dylib" "${GOOGLE_ANGLE_DIR}/macOS/libGLESv2.dylib")
  target_link_libraries(${target} PRIVATE "${google_angle_libs}")

Then I added a target property to make sure that the rpath is correct:

set_target_properties(${target} PROPERTIES
    INSTALL_RPATH "@executable_path/../Frameworks"
)

And then finally, I copy the libraries during install

install(
    FILES "${google_angle_libs}"
    DESTINATION "$<TARGET_BUNDLE_DIR_NAME:${target}>/Contents/Frameworks"
)

So what happens, is that during the build, cmake generates an executable with the following rpath:

Load command 26
      cmd LC_RPATH
      cmdsize 88
      path /Volumes/Vault/tmp/misc-projects/test-raylib/external/google/angle/macOS (offset 12)

and the libraries are never copied inside the .app (bundle) because they are found in the source tree… So it just works.

During install, due to the property INSTALL_RPATH that is set, cmake will do the right thing and remove the hard-coded rpath and replace it with the correct one and because the libraries are copied inside the bundle (under Contents/Frameworks) they will be found as well:

Load command 29
      cmd LC_RPATH
      cmdsize 48
      path @executable_path/../Frameworks (offset 12)

Yes, that logic only supports one rpath and doesn’t do the inheritance logic. It’s (one of the reasons) why I wrote those scripts.

@ben.boeckel Thank you for sharing your scripts. I wished I I had seen this sooner as it might have saved me the huge amount of time I spent. Given how complicated the logic is, it is very obvious that it is not a trivial problem…