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?