All I wanted to do was give my console app to a non-technical user.

I had made a command line based app for a college student. It hooked up to a new, not fully released API from the Oxford dictionary. The API, Oxford Researcher, was only available to universities at the time. You type in a word with all sorts of options. The application than spits back information like what regions of the world the word came from, quotes based on the first usage of the word. It used the System.CommandLine library and took an enormous amount of parameters and options.

I knew the student I was building it with wanted to use the app on Mac. Part of why I picked .NET 5 at the time was the easy cross platform support. However, with my limited intelligence porting to Mac was not as easy as I hoped. I even ended up renting a virtual mac from MacInCloud. It was terrible.

Here is how little I knew about mac. I did not realize that MacOSX does not even use .exe files, or any extension like that. That is how naive I was.

The .exe file suffix is just a hold over from MS-DOS days. .EXE replaced the self-contained executable .COM programs. While Linux uses ‘ELF’ macOSX uses the Mach-O format for executables. Further, what makes a file executable on Mac is if certain bits in the file flag the file as executable. This technique was stolen from Unix.

So, obviously, we can not just send over an .exe. Many different files can be flagged as executable on mac, but I found Mac user’s do not like to receive anything other than a dmg file.

Here is what I pinned my hopes on: simply publishisng a self-contained executable to Mac with dotnet publish. After that, I was going to use hditul - on mac - to create the DMG file. Voila!

DMG files (Apple Disk Image File) are just macOS disk image files. They operate similar to ISO files on Windows. You double click the .dmg file and it mounts a disk image on the desktop. To finish your installation, you typically drag the app from the mounted disk image to your Applications folder. We don’t want to run the app rom the .dmg, but allow the user to drag it to their applications folder. Technically, the preferred term is attach and not mount as this is disk image and not a physical disk.

I was surprised to find out that I could not just open Visual Studio for Mac and hit publish.

Visual Studio for Mac doesn’t provide options for publishing to Mac (How strange). Instead you to have to use the CLI. Which I prefer anyway, as it makes me feel smarter than I am.

Here is what I ended up doing:

First we publish our project for Mac. We can only run our app on Mac OSX higher than macOS 10.12 Sierra. Although, Sierra is no longer supported https://docs.microsoft.com/en-us/dotnet/core/install/macos.

Although we are going to create a DMG file (disk image) – we are going to publish our project as a single file. We are also going to make the app ‘self-contained’ so that the user doesn’t have to install the .NET framework. It is important to keep in mind that just because an app is self-contained – and includes all required files. The native dependencies of .NET must be present on the system before the app runs. Sooooooooooo disappointing.

(We are not going to bother trimming the self-contained app - as we can now do in .NET 6)

dotnet publish -c Release -r osx-x64 --self-contained true /p:PublishSingleFile=true

Now if we go to the release folder:

cd bin/Release/net5.0/

Not go into the osx-x64 folder:

cd osx-x64

Notice we have all of the files. What!! [mac-1]

[mac-2]

Even though we specifically specified a single file, dotnet publish spits out a million files.

The single file option creates all the files normally, that you would need if you did not specify single file, then (in addition to all those crazy files) it creates a single file.

In .NET Core 3.x publishing a single file produced exactly one file. If you ran that file - the app was extracted to a folder and run from there. I am using .NET 5 – managed DLLs are now included with the app. Those .dll files are extracted and loaded in memory so the app will work. This way, on our target platform, we don’t need to create a folder, extract the files to that folder, etc. It all happens in memory. If one wants to skip these files you can include the property

IncludeNativeLibrariesForSelfExtract 

to true

If I add IncludeNativeLibraryesForSelfExtract to my *.csproj file on mac [mac-3] becomes: However, we can skip modifying the *.csproj file: and just add

-p:IncludeAllContentForSelfExtract=true

to the end of dotnet publish

dotnet publish -c Release -r osx-x64 --self-contained true /p:PublishSingleFile=true /p:IncludeAllContentForSelfExtract=true

I get the following output:

If we are using .NET 6 the runtime is pre-linked into the bundle on all platforms.

Since our app is called “hello-test” the actual file we need is “hello-test” [mac-4] [mac-5]

Double clicking this file does indeed work: [mac-6] [mac-7]

What we want is the reduced mess inside the publish folder:

ls -ld -- */

Ignore the ref folder.

Inside the ref folder are “Reference Assemblies” which MsBuild uses to speed up the build process. It is only helpful if your project is referenced by other projects so whatevs.

Back to the publish folder.

[mac-8]

I thought you told me this was going to be a single file?

Double clicking hello-test will run the app [mac-9]

Note - the app requires everything in this folder. (It does not require the debugging .pdb file The promise of a true single file app was a lie. A filthy lie.

The dynamic libraries (.dylib) can be complicated to distribute. We have .NET dependencies that will only compile as dylibs. [mac-10] Trying to fun “hello-test” by itself - does not work.

As on Mac apps are actually bundles of files. They are entire folders that look like a single file executable to the user. The nice thing about this is that the user drags around the app and everything is included at all time (as an entire folder of all dependencies are being moved with the executable at all times).

So we want to turn our publish folder into a bundle is to add the .app extension to the folder. When the user clicks the our folder errr now app - it will run the executable file. Right click –> Rename Type .app at the end Click “Add” [mac-11]

Notice our attractive folder icon has been replaced with a hideous, threatening, ghost-busters esq icon: [mac-12] [mac-13]

Alas, we can not open the application “publish” because it may be damaged or incomplete. [mac-14]

It is hilarious that a hello-world app on .NET 5 is 63.5 megabytes! I guess that is because we included all the framework stuff.

[mac-15]

Trying to open the file in terminal we get more information on the problem:

"KLSNoExecutableErr: The executable is missing"

First we are going to create a dmg file using hdiutil. hdiutil uses the DiskImage framework to manipulate disk images.

To get my more complex console app to run I had to make sure I changed hard-coded folder path strings in the \ windows way. i.e., replacing this

StreamReader reader = new StreamReader(".\\keys.txt");

with:

var keysFilePath = Path.Combine(Environment.CurrentDirectory, "keys.txt");
StreamReader reader = new StreamReader(keysFilePath);

For example I commented out all my logging set up code:

// Logging disabled for mac osx
            /*
            string directoryPath = string.Concat(Environment.CurrentDirectory, "\\logs");

            try
            {
                // Determine whether the directory exists
                if (Directory.Exists(directoryPath))
                {
                    Trace.WriteLine("The logs path already exists.");

                }
                else
                {
                    // Try to create the directory
                    DirectoryInfo di = Directory.CreateDirectory(directoryPath);
                }
                string fullPath = string.Concat(Environment.CurrentDirectory, $"\\logs\\Log_OxfordApplication_{DateTime.Now.ToString("yyyyMMdd-HHmm")}.txt");
                Trace.WriteLine("Path is {0}", fullPath);
                
                /*
                TextWriterTraceListener tr1 = new TextWriterTraceListener(System.Console.Out);
                Trace.Listeners.Add(tr1);

                TextWriterTraceListener tr2 = new TextWriterTraceListener(System.IO.File.CreateText(fullPath));
                Trace.Listeners.Add(tr2);
            }

            catch (Exception e)
            {
                xConsole.WriteLine($"The process failed: {e.ToString()}");
            }
            finally { }

            Trace.WriteLine("Leaving Main method.");
            Trace.Flush();
            */

Building on Windows and sending the value to Mac OSX does not work.

I was too stupid to realize I have to move my keys.txt file to users/admin/

The application required private keys which were used to access the API. Forgetting to load the keys into the MAC admin area - as opposed to just keeping them relative to the path of the app – as I had on Windows – created all sorts of problems.

Copy keys.txt to Users/Admin/

mv keys.txt /users/admin/

Turning Executable into a document

I was having the problem that as soon as I uploaded my Unix Executable to share via Google Drive it was being changed to a document. This has to do with permission issues. The trick is to first zip the file, that way no email or document sharing program will mess with the permissions.

After all that, it finally worked.