Plugin Dynamic Library Deployment for Mac

There are some strange issues on Mac trying to build a plugin needs its own dylib. The approach that works on most platforms is to place the dynamic libraries in the plugin Binaries directory and use PublicAdditionalLibraries (possibly with RuntimeDependencies as well).

1.)
So we have our fmod dylib in the plugin’s Binaries directory:
FMODStudio/Binaries/Mac/libfmodL.dylib

We add that path to PublicAdditionalLibraries. That works enough to get into the editor. But when launching or packaging a build, UE4 produces this build error:

ERROR: AutomationTool terminated with exception: AutomationTool.AutomationException: BUILD FAILED /…/Binaries/Mac/ProjectName.app/Contents/MacOS/libfmodL.dylib was in manifest but was not produced.

And sure enough the dylibs are not copied into that location, so the error is correct in a sense.

2.)
Strangely enough, if the dylib is placed somewhere else, such as:
FMODStudio/Libs/Mac/libfmodL.dylib

And then referenced via PublicAdditionalLibraries, then the dylib does get into the above path for launch or deployment, and it correctly loads that library so that the references work when running the game.

However, approach 2.) suffers from a problem in that the editor itself will fail to start up. The plugin UE4Editor-FMODStudio.dylib is built in such a way that it has a rpath entry of “libfmodL.dylib” without any relative path back to its Libs directory. So unless the fmod dylib is in the Binaries directory, then the editor fails to load.

3.)
As a workaround, if we have our fmod .dylib in both the Binaries and a Libs directory, and reference the Libs directory using PublicAdditionalLibraries, then we get both the editor starting up, and deploying working. That seems a hack though.

Apologies for the delay. I’m working on a solution for you.

My first thought was that you should do what ICU does - store the dylib in Engine/Binaries/ThirdParty/ and let the RPATH handle that, but apparently our current code in MacToolChain that sets RPATHs up does not work with plugins correctly. I’m working on a fix for that.

Apologies for such a long delay. I got sidetracked and couldn’t properly test this until now.

The change is very simple: in AddLibraryToRPaths in MacToolChain.cs replace this line:

if (!RelativePath.Contains(LibraryDir) && !RPaths.Contains(RelativePath))

with:

if ((!RelativePath.Contains(LibraryDir) || RelativePath.Contains("/ThirdParty/")) && !RPaths.Contains(RelativePath))

After that, make sure the id of the lib in libfmodL.dylib is using @rpath. You can check that using otool tool. In Terminal, do this:

otool -L libfmodL.dylib

If the first entry on the list is not @rpath/libfmodL.dylib, do this in terminal:

install_name_tool -id @rpath/libfmodL.dylib libfmodL.dylib

Finally, in Mac section of your .Build.cs file, add something like this:

string Path = "../../Binaries/Mac/libfmodL.dylib";
PublicDelayLoadDLLs.Add(Path);
PublicAdditionalShadowFiles.Add(Path);
RuntimeDependencies.Add(new RuntimeDependency(Path));

Let me know if you run into any issues with that.

Also, as you most likely noticed by now, UDN formatting is failing. I hope this is readable enough :confused:

Just a funny FYI but I keep getting emails about this thread… The @R part in Mike Trepka’s comment, which keeps getting quoted, seems to be linking to my user profile… So i’m getting notifications about this thread.

Thanks for investigating, I look forward to any fixes.

I’m definitely interested in a solution where all the files are under a single directory (under Plugins/FMODStudio). That keeps the install simple. We did initially have a process where plugin files where under a plugin directory and libs went to Engine/Binaries/ThirdParty/FMODStudio, but that two step install was confusing to some users. Dragging single folder in to install is a much simpler workflow.

Good point. The solution I have in mind should allow you to do that. I’ll make sure to test this use case and I hope to have it ready for you tomorrow. The changes to the code should be minimal (within AddLibraryPathToRPaths function in MacToolChain.cs).

I was just encountering this issue today.

With this change is the suggested location for storing these files still in Engine/Binaries/ThirdParty/ExampleLib/OS/ e.g. Engine/Binaries/ThirdParty/ExampleLib/MacOSX/libExampleLib.dylib ?

In some instances I am seeing my library being linked as @loader_path/libMyLib.lib instead of libMyLib.lib. I still haven’t discovered why that is happening, but the fix for that was to add this to LinkFiles in MacToolChain.cs (above or below the existing install_name_tool change):

LinkCommand += "; " + ToolchainDir + "install_name_tool -change @loader_path/" + LibraryFileName + " " + DylibsPath + "/" + LibraryFileName + " \"" + ConvertPath(OutputFile.AbsolutePath) + "\"";

I don’t know if this is a safe change to make overall but it did solve our issues for today.

With this change you should be able to store your dylib anywhere, including folders like Engine/Plugins/Runtime/MyPlugin/Binaries/Mac.

Unfortunately I’m still having the issue of rpath not getting setup properly for the lib. Files are located in Engine/Binaries/ThirdParty/MyLib/Mac/libMyLib.dylib.

I have that dylib added to PublicAdditionalLibraries, PublicAdditionalShadowFiles, PublicDelayLoadDLLs, and RuntimeDependencies.

MacToolChain.cs looks like this:
if ((!RelativePath.Contains(LibraryDir) || RelativePath.Contains(“/ThirdParty/”)) && !RPaths.Contains(RelativePath))

OK. I’ll need more info. Go to terminal, cd to Engine/Binaries/ThirdParty/MyLib/Mac and check what otool -L libMyLib.dylib says.

Then, go to wherever the editor/engine lib that links to libMyLib.dylib is, and type otool -l UE4Editor-libname.dylib (that’s lowercase L). This command will show you a list of executable sections and near the end a list of load commands. Some of them should be LC_RPATH load commands. Check if one of these contains the path to the folder libMyLib.dylib is in.

otool -L libMyLib.dylib shows @rpath/libMyLib.dylib
otool -l UE4Editor-MyPlugin.dylib does not show an LC_RPATH entry to my directory.

LC_RPATH has two entries for @loader_path/ and @executable_path/, there is no relative path to the location of my dylib in Engine/Binaries/ThirdParty

Sorry! I forwarded this to UDN team, hopefully it’s something they can fix.

Hi Robert,

Thanks for the report about the inadvertent notification. It’s a bug, and we’ve been able to repro it in other circumstances. Unfortunately, I don’t know how to remove you from getting this alert. I’m creating a bug and we’ll try and correct the issue as quickly as possible.

I apologize for the inconvenience.

Josh Fleming

Developer Support Manager

Epic Games

Maybe you didn’t rebuild UnrealBuildTool? I totally forgot it’s not built build Build.sh script any more and you have to re-run GenerateProjectFiles script :confused:

Doh! That was totally it, I was using Build.sh to build the project and didn’t compile UBT. My bad on that.

The setup for loading dynamic libraries across platforms/plugins/dependencies seems to be pretty scattered. For instance, PhysX does LoadLibraryW calls only on Windows while Steam does FPlatformProcess::GetDllHandle. Is the FPlatformProcess::GetDllHandle process deprecated with this rpath change, or is it even needed at all?

Having some consistency and guidance on this would be fantastic.

Great, I’m glad that was it.

PhysX is doing it wrong, but Windows is the only platform that uses dynamic libs for PhysX, so it doesn’t really matter. Mac should start using dylibs for PhysX when we upgrade to 3.4, then I’ll change this code to use FPlatformProcess::GetDllHandle.

As for FPlatformProcess::GetDllHandle, it’s definitely not deprecated and is the correct, cross platform way of loading dynamic libraries in UE4. But you only need this for libraries that you don’t link to. If I remember correctly, we do this for Steam, because it’s optional, so the Steam lib may not be bundled with the game. If you link to your libMyLib.dylib, you don’t need to load it with FPlatformProcess::GetDllHandle.

Good to know. I’m using FPlatformProcess::GetDllHandle for the additional dylib dependencies of my library file, but the handle returned is nullptr. I’m doing FPlatformProcess::GetDllHandle(“libMyDependency.dylib”) with the file located at Engine/Binaries/ThirdParty/MyLib/Mac with no luck. Should I be adding it the dependency to the delay load DLL list for Mac with the path to it in Binaries/ThirdParty?

If FPlatformProcess::GetDllHandle is returning nullptr, this means it couldn’t find the dylib in Binaries/Mac folder nor inside the app bundle. If it’s not in either of these places, you need to use a path relative to Engine/Binaries/Mac as param, not just the library name. In your case: FPlatformProcess::GetDllHandle(“…/ThirdParty/MyLib/Mac/libMyDependency.dylib”)

Alright, almost there. I have a plugin that is doing a PrivateDependencyModuleNames.Add, when I do a otool -L the library I am linking to is a @loader_path/libMyLib.lib and not a rpath. If I do a otool -l I am seeing the Engine/Binaries/ThirdParty/MyLib/Mac rpath being setup. Thoughts?