I'm trying to create a macOS .NET core application that's self contained and is a universal x86_64 / arm64 app. I don't have Visual Studio for Mac installed, and given Microsoft is retiring it, I'm trying to work out a method for doing this that doesn't rely on it.
I closely read through this question: Can I create a .net6 app for macOS with both x64 and arm64 support? ...but its accepted answer doesn't work, and seems to only apply to those using Visual Studio for Mac, which I am not. I need a solution that works specifically for using .NET core's command line tools. (edit) Further, that question is not about creating a self-contained release.
Normally with .NET core app, you would create a ready-to-distribute build using:
dotnet publish -c Release -r osx-x64 --self-contained
or
dotnet publish -c Release -r osx-arm64 --self-contained
This will create a folder in your build directory with the self-contained app. It won't be assembled into a application bundle, but it will contain the .NET core runtime including all of the dlls necessary and be self contained.
Unfortunately, it will also only be built for one architecture, either x86_64 or arm64, depending on the -r
argument passed to dotnet publish
.
I can assemble the application bundle myself by copying all of these files into the correct places. And I can create universal binaries of all the executables and dylibs by taking a release created for x86_64 and using lipo
to combine it with a release created for arm64.
However, all of that still isn't enough because all of the .NET core runtime DLLs copied by dotnet publish
are still platform specific. Trying to run the app on the architecture that was not the one you used to build the app will result in this error:
Failed to load System.Private.CoreLib.dll (error code 0x8007000B)
Path: /path/to/app/Contents/MacOS/System.Private.CoreLib.dll
Error message: An attempt was made to load a program with an incorrect format.
(0x8007000B)
Failed to create CoreCLR, HRESULT: 0x8007000B
I'm now totally stuck. There's no documentation from Microsoft about how to do this. It's not even clear if it's possible. It ought to be, assuming there are runtime DLLs that are platform independent and there's some way to tell dotnet publish
to use those instead of platform-specific ones.
Can anyone help?
Edit: One possible option is to bundle both the x86_64 and arm64 release builds into a single application bundle and then having the bundle's executable be a shell script that conditionally launches one depending on the system architecture. Any shared resources could be handled by symlinks. But I'm wondering if there's any better options, like perhaps using a platform independent (framework-dependent?) runtime, if it exists.
Edit 2:
I've discovered that several of the runtime DLLs produced by dotnet publish --self-contained
are the same between arm64 and x86_64. So another improvement that can be made to the above method is to only have one copy of each of those DLLs and use symlinks between the arm64 and x86_64 runtimes.
Really the big question at this point is whether it's possible to have platform-independent / framework-dependent (to use Microsoft's terminology) / arm64 + x86_64 universal runtime DLLs.
After more research, I've developed a method of creating a universal application that seems to be as space efficient as possible, and avoids using
PublishSingleFile
, which is apparently important for games developed using MonoGame, and may be important in other use cases as well.It doesn't seem possible to make a truly self-contained .NET release that uses a universal version of the .NET runtime, because no such runtime exists. Some of the DLLs included in a self-contained release have non-managed code (i.e. native code), and therefore those DLLs are specific to a platform and CPU architecture.
However, this only accounts for roughly half of the DLLs in the .NET runtime. The other half are only managed code, and therefore can run on either x86_64 or arm64 macOS. We can take advantage of this to create a universal application that contains both x86_64 and arm64 runtimes with as little redundancy as possible.
First, create both an x86_64 and arm64 release of your application with:
I recommend having
PublishReadyToRun
,TieredCompilation
, andPublishTrimmed
set to false, but you can change those as you require.Then create an application bundle structured like this:
Where $EXECUTABLE is the name of your .NET executable produced by
dotnet publish
.The executable file in the macOS directory should contain a shell script that looks like this:
This will launch the release of the correct architecture when the user launches the application. Note that macOS (possibly due to a bug) will always launch this shell script in an x86_64 process on arm64 systems that have Rosetta 2 installed, regardless of the value of the
LSArchitecturePriority
key in Info.plist. So we need to usesysctl
in order to truly figure out if we're running on an Apple Silicon mac, becauseuname
will indicate the system is x86_64 when running under Rosetta 2.The last step is to copy any DLLs that only contain managed code (and are therefore compatible with both x86_64 and arm64 systems) from the
osx-arm64
/osx-x64
directories intoshared
, and then replace those DLLs inosx-arm64
andosx-x64
with symlinks to the same DLL inshared
. The symlinks must use relative paths. You can tell which DLLs only contain managed code because they will be identical between theosx-arm64
andosx-x64
directories.If your project requires any external dylibs, make sure they're universal arm64 / x86_64 binaries, and copy them into
shared
as well. Then create symlinks to them inosx-arm64
andosx-x64
, like you did with the DLLs.This process would be quite difficult and annoying to do by hand, so I've written a bash script that will do it automatically. You can run this script from anywhere inside your .NET project directory and will automatically generate x86_64, arm64, and universal applications. Be sure to modify the variables at the top of the script as necessary, and add in lines to copy in any needed dylibs and other resources into the application bundle as needed. (See the comments in the script for where to make these changes.)