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 (C++/Objective-C) compiled to .o object files
- Scheme modules compiled to .so shared libraries using csc -s -J
- Main app compiled with -deployed -private-repository for bundle deployment
- Dependencies (eggs + system dylibs) bundled for distribution
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
- Module separation: Each .scm file becomes a .so loaded at runtime
- -deployed -private-repository: Critical for the main binary to find modules
- -headerpad_max_install_names: Essential for install_name_tool to work
- Eggs installed into bundle: Using CHICKEN_REPOSITORY_PATH redirection
- Launcher wrapper: Avoids corrupting CHICKEN binaries when rewriting paths
Hope this helps anyone else trying to build distributable macOS apps with CHICKEN Scheme!