Distributing Crystal apps on Mac : Is this correct?

Distributing a Crystal app on macOS without static linking involves bundling the necessary shared libraries and dependencies with your application. You’ll also need to create a script to ensure that the application can locate these libraries at runtime. Here’s a general approach to achieve this:

  1. Compile Your Crystal Program: You can start by building your Crystal application using the crystal build command. You’ll have to include the required shared libraries and dependencies.

  2. Identify Dependencies: Use a tool like otool to inspect the dynamic libraries that your binary depends on. Run this command on your compiled binary:

    otool -L your_executable
    

    This will list the shared libraries that your executable is linked against.

  3. Bundle Shared Libraries: Copy the required shared libraries into a directory that will be distributed with your application. You might place these in a libs folder within your app’s bundle.

  4. Update Library Paths: Use the install_name_tool command to change the paths to the shared libraries within your binary. Point them to the location where you’ve bundled the libraries in your distribution. You’ll want to use a command like this for each shared library:

    install_name_tool -change old_path new_path your_executable
    

    where old_path is the current path to the library, and new_path is the relative or absolute path to the library within your app’s bundle.

  5. Create a Startup Script: Create a startup script that sets the DYLD_LIBRARY_PATH environment variable to include the directory where you’ve bundled the shared libraries. This script would then execute your Crystal binary. Here’s a basic example:

    #!/bin/bash
    export DYLD_LIBRARY_PATH="./libs:$DYLD_LIBRARY_PATH"
    ./your_executable
    
  6. Package Your Application: Bundle your compiled binary, the shared libraries, and the startup script together into a distributable format, such as a .dmg or .tar.gz file.

  7. Distribute Your Application: Share your packaged application with users through the appropriate channels, whether it’s a direct download from your website, an app store, etc.

By following this approach, you’ll ensure that the shared libraries your Crystal app depends on are included with the app and that they are properly located at runtime, even though they are not statically linked into the binary. It does require careful management of these shared libraries, but it makes distribution on macOS possible.

3 Likes

A possible Makefile to automate the process :slight_smile:

# Variables
APP_NAME = your_executable
LIBS_DIR = ./libs
SRC_DIR = ./src
BUNDLE_DIR = ./bundle
STARTUP_SCRIPT = startup.sh

# Build your application
build:
	crystal build $(SRC_DIR)/main.cr -o $(BUNDLE_DIR)/$(APP_NAME)

# Copy and update library paths
libs:
	mkdir -p $(BUNDLE_DIR)/$(LIBS_DIR)
	# Use otool to identify and copy the required libraries, then update their paths
	otool -L $(BUNDLE_DIR)/$(APP_NAME) | awk '/path_to_required_lib/ { system("cp " $$1 " $(BUNDLE_DIR)/$(LIBS_DIR)"); system("install_name_tool -change " $$1 " $(LIBS_DIR)/" $$1 " $(BUNDLE_DIR)/$(APP_NAME)"); }'

# Create startup script
startup-script:
	echo '#!/bin/bash' > $(BUNDLE_DIR)/$(STARTUP_SCRIPT)
	echo 'export DYLD_LIBRARY_PATH="$(LIBS_DIR):$$DYLD_LIBRARY_PATH"' >> $(BUNDLE_DIR)/$(STARTUP_SCRIPT)
	echo './$(APP_NAME)' >> $(BUNDLE_DIR)/$(STARTUP_SCRIPT)
	chmod +x $(BUNDLE_DIR)/$(STARTUP_SCRIPT)

# Package your app
package:
	tar -czvf $(APP_NAME).tar.gz -C $(BUNDLE_DIR) .

# Clean build artifacts
clean:
	rm -rf $(BUNDLE_DIR) $(APP_NAME).tar.gz

# Main targets
all: build libs startup-script package

.PHONY: build libs startup-script package clean all

3 Likes

Nice, @serge-hulne. With a couple of tweaks, I was able to build one of my test apps with external lib deps. Thanks!

1 Like

That’s great.
It would be even better if I didn’t have to set DYLD_LIBRARY_PATH.

Is there any possibility of using @loader_path or specifying -rpath to make it even easier?

Mac dynlib bundler : Could be useful to distribute Crystal apps:

Are those shared libs not architecture dependant?

There are macs with a range of different processor architectures nowadays (at least if you want to support older machines)
I thought the libraries would need to be compiled for a target architecture in the same way as any other binary?
Or is there some special magic happening.

(i encountered this once with “Invidious”, which has a bundled binary blob library, and that prevented me from running the software on a RaspberryPI, because the lib was x86)

You are correct. The problem which is solved here is not a cross-compilation situation, but a situation in which one distributes an executable app between identical machines in the case in which only one of said machines has got the development libraries installed.

Technically yes, you need a different binary for every architecture.

For macOS there are some special cases though.

  1. macOS has a compatibility layer called Rosetta which allows executing x86-64 code on aarch64 (Apple Silicon). I hear it’s super fast. Might not always work, but usually it does.
  2. macOS easily allows merging binaries for x86-64 and aarch64 into an universal binary. It’s just two binaries in one and the OS runs the version that matches the host architecture. But for distribution it’s just a single file, so you don’t need to worry about which arch you deploy to.

These concepts should more or less work similarly on other operating systems, but they’re really common with macOS which has good tooling for that.

2 Likes