You are looking at historical revision 45315 of this page. It may differ significantly from its current revision.

Building a macOS App Bundle with CHICKEN Scheme

Overview

The architecture for my case is:

Native Code Compilation

C++/Objective-C components are compiled normally:

video_player.o: video_player.cpp
	c++ $(CXXFLAGS) $(FFMPEG_CFLAGS) -c $< -o $@

file_dialog.o: file_dialog.mm
	c++ $(CXXFLAGS) -x objective-c++ -c $< -o $@

Scheme Module Compilation

Each Scheme module is compiled as a shared library:

CSC_MODULE_FLAGS = -d3 -O0 -c++ -s -J -L "-Wl,-headerpad_max_install_names"

# Example module with native dependencies
video.so: video.scm av_player.o
	csc $(CSC_MODULE_FLAGS) -C "$(CXXFLAGS)" -L "$(LDFLAGS)" $< av_player.o -o video.so

Key flags:

-s
compile to shared library (.so)
-J
generate import library (.import.scm)
-headerpad_max_install_names
reserve space for install_name_tool to rewrite paths later

Import libraries are then compiled to .import.so for deployment:

%.import.so: %.import.scm
	csc -s -O2 $< -o $@

Main Application

The main app.scm dynamically loads modules at runtime:

app: $(SCHEME_SOS) $(SCHEME_IMPORT_SOS) app.scm
	csc $(CSC_FLAGS) -deployed -private-repository \
		-C "$(CXXFLAGS)" \
		-L "-Wl,-headerpad_max_install_names" \
		app.scm -o $@

Key flags:

-deployed
sets rpath for bundled libraries
-private-repository
load extensions from executable's directory

Bundle Creation

The bundle follows the standard macOS structure:

MyApp.app/
  Contents/
    MacOS/
      MyApp              # main binary
      *.so               # Scheme modules
      *.import.so        # import libraries
    Resources/
      fonts/
      ...
    Frameworks/
      *.dylib            # bundled dynamic libraries (for distribution)

Creating the bundle:

mkdir -p MyApp.app/Contents/MacOS MyApp.app/Contents/Resources
cp app MyApp.app/Contents/MacOS/MyApp
cp *.so MyApp.app/Contents/MacOS/
cp *.import.so MyApp.app/Contents/MacOS/

Installing eggs directly into the bundle:

CHICKEN_REPOSITORY_PATH=MyApp.app/Contents/MacOS \
CHICKEN_INSTALL_REPOSITORY=MyApp.app/Contents/MacOS \
chicken-install medea http-client ...

Self-Contained Distribution

For distribution to machines without Homebrew, dylibs must be embedded.

First, identify and copy all Homebrew dylibs:

for lib in $(otool -L MyApp.app/Contents/MacOS/*.so | grep '/opt/homebrew' | awk '{print $1}' | sort -u); do
    cp "$lib" MyApp.app/Contents/Frameworks/
done

Then rewrite the load paths using install_name_tool:

# For each dylib in Frameworks
install_name_tool -id "@executable_path/../Frameworks/libfoo.dylib" libfoo.dylib

# For each .so module
install_name_tool -change "/opt/homebrew/lib/libfoo.dylib" \
    "@executable_path/../Frameworks/libfoo.dylib" module.so

Important: I found that running install_name_tool directly on the CHICKEN binary can corrupt it. My workaround is a small C launcher that sets DYLD_LIBRARY_PATH and exec's the real binary:

// launcher.c
#include <unistd.h>
#include <libgen.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[]) {
    char *dir = dirname(argv[0]);
    char fw_path[1024];
    snprintf(fw_path, sizeof(fw_path), "%s/../Frameworks", dir);
    setenv("DYLD_LIBRARY_PATH", fw_path, 1);

    char real_bin[1024];
    snprintf(real_bin, sizeof(real_bin), "%s/MyApp.bin", dir);
    execv(real_bin, argv);
    return 1;
}

Key Takeaways

  1. Module separation: Each .scm file becomes a .so loaded at runtime
  2. -deployed -private-repository: Critical for the main binary to find modules
  3. -headerpad_max_install_names: Essential for install_name_tool to work
  4. Eggs installed into bundle: Using CHICKEN_REPOSITORY_PATH redirection
  5. Launcher wrapper: Avoids corrupting CHICKEN binaries when rewriting paths

Hope this helps anyone else trying to build distributable macOS apps with CHICKEN Scheme!