Help: Document that SHARED IMPORTED libraries should set IMPORTED_SONAME
Summary: I have a shared library which is created at buildtime in an unorthodox way (IE, the compiler is not invoked). Following a tip on stackoverflow I have done this by creating a custom command attached to a custom target to generate the shared library, and then a normal library target (dependent on the custom one) created with
add_library(... SHARED IMPORTED). However when I create the library this way, the "rpath" (a macOS-specific dynamic linking field) for the library is not correctly set on built executables that link the library. Worse, this behavior is not idempotent: I find the rpath is incorrectly set on first build, but can be correctly set on successive builds.
Background, rpath: Dynamic libraries on macintosh have an “install name” which is the absolute path the library believes it is installed at. When you link a binary against the library, the linker saves the library's “install name” into the executable to load from later. Although this path is absolute it can contain several aliases, such as “@rpath”, which dyld will expand as appropriate when the executable is run. An executable has a list of “rpath search paths” which dyld will try in turn for any dynamic library whose install name contains an @rpath. You can check a dylib’s install name using
otool -L, and you can check an executable’s linked libraries and rpath search paths with
otool -l. You can manually alter install-names and rpath search paths on an existing executable using
install_name_tool. A longer explanation of all this is here.
Background, project: I have a minimal repro below which does not depend on my project, but here is how this came up for me in practice:
I contribute to an open source VR game engine. The engine incorporates a number of gamedev libraries into a single package. We have a CMake file that builds the various libraries as subprojects and then builds the executable. When doing development on the engine itself, instead of building the package for install we will often for convenience just build the executable and then run it out of the CMake build folder.
Two of our libraries are closed source, so these are brought in with
add_library(… SHARED IMPORTED). These two libraries currently work in our engine on Windows and Android but not mac. I am trying to add mac support.
One of these libraries (Oculus Audio, a sound spatializer) has a complication. As shipped by the vendor, it has an incorrect install name (it points to an actual literal absolute path on what appears to be the vendor’s build server), which requires fixing with
install_name_tool before it is used.
I added this library to our CMake file, using the custom-target trick mentioned above and generating the library by copying it into the build directory and then running
install_name_tool on the copy. After building, I find the executable could not run; the Oculus Audio library was not found. Checking the built executable with
otool -l, I saw that the rpath for Oculus Audio had not been set.
Then something very strange happened. While debugging this issue, I made a change to my CMakeLists.txt, then undid the change, then saved. In other words I updated the modification time of the CMakeLists.txt without changing anything. I did another build. After this meaningless rebuild, the application worked and checking the executable with
otool -l I found as expected an rpath into my build directory:
Load command 27 cmd LC_RPATH cmdsize 56 path /Users/mcc/work/gh/lovr/build/ovraudio (offset 12)
How to reproduce:
You can download the experimental branch of our VR engine in which I added the mac spatializer support (use branch mac-spatializer-test), or this minimal example repo (use branch ovraudio) which only links the two relevant libraries. (If you download the mainline project you must initialize all git submodules.)
You also must download Oculus Audio from here.
In either of these repos, you can test by running these commands:
(rm -rf build && mkdir build) (cd build && cmake -DLOVR_USE_OCULUS_AUDIO=1 -DLOVR_OCULUS_AUDIO_PATH=/Users/mcc/Downloads/spatializers/AudioSDK ..) (cd build && cmake --build .)
(Obviously you must replace LOVR_OCULUS_AUDIO_PATH with a path to your unzipped download of Oculus Audio.)
Once you have built, run
./build/lovr and you will see this error:
dyld: Library not loaded: @rpath/ovraudio64.dylib Referenced from: /Users/mcc/work/gh/lovr/temp/cmake-rpath-test/./build/lovr Reason: image not found Abort trap: 6
At this point, you can run
otool -l ./build/lovr and see the rpath is not present. If you like, you can repeat the step
(cd build && cmake --build .) here and verify nothing changes.
Now, try updating the modification time of CMakeLists.txt and rebuilding:
touch CMakeLists.txt (cd build && cmake --build .)
Re-running the test, you will see that
./build/lovr now works and that
otool -l ./build/lovr shows an rpath into build/ovraudio.
I have seen quirks in the past with CMake where re-running a build produces different results on the second try, and these are among the most frustrating glitches possible with a CMake script. However, usually this only happens if the CMakeLists is doing something obviously foolish. In this case, the CMakeLists.txt seems to be following the rules: It is adding a SHARED IMPORTED library at a path, and the script that ensures the library exists at that path is properly sequenced using
add_dependencies to have run first. This should work.
I speculate that whatever logic in CMake handles gathering and assigning the rpaths runs very early, before any targets are executed. If the library file is not found at that early moment, then the library is opted out of getting its rpath passed on to any executables that link it. This would explain why the rpath is missed on the first build (when the file does not exist yet) but is correctly assigned on the second build (when the file exists because it was created the last time we built).
While debugging this error, I tried reordering the CMake commands in various ways and explicitly setting BUILD_RPATH. None of this made any difference. Assuming I understand correctly how BUILD_RPATH works, it is very alarming that the manually assigned BUILD_RPATH fails to be inherited just because the file is missing, since in this case the rpath is not derived from the file at all. (EDIT: This paragraph was based on a misunderstanding.)
./build/lovr in the minimal test case above should work properly on first build. In general, if CMake derives state for a target from a file on disk, it should not attempt to check that state until the dependencies of that target have completed running.