Wiki
Download
Manual
Eggs
API
Tests
Bugs
show
edit
history
You can edit this page using
wiki syntax
for markup.
Article contents:
== Building a macOS App Bundle with CHICKEN Scheme [[toc:]] === 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: <enscript highlight="make"> 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 $@ </enscript> === Scheme Module Compilation Each Scheme module is compiled as a shared library: <enscript highlight="make"> 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 </enscript> Key flags: ; {{-s}} : compile to shared library ({{.so}}) ; {{-J}} : generate import library ({{.import.scm}}) - needed for compilation only ; {{-headerpad_max_install_names}} : reserve space for {{install_name_tool}} to rewrite paths later '''Note on import libraries:''' The {{-J}} flag generates {{.import.scm}} files which can be compiled to {{.import.so}}. These are only needed at '''compile time''' for modules that depend on other modules. They are '''not needed at runtime''' and should be excluded from the final bundle to reduce size. === Main Application The main {{app.scm}} dynamically loads modules at runtime: <enscript highlight="make"> 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 $@ </enscript> 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 # launcher (see below) MyApp.bin # real CHICKEN binary *.so # Scheme modules (runtime only, NOT .import.so) Resources/ fonts/ ... Frameworks/ *.dylib # bundled dynamic libraries (for distribution) Creating the bundle: <enscript highlight="sh"> mkdir -p MyApp.app/Contents/MacOS MyApp.app/Contents/Resources cp app MyApp.app/Contents/MacOS/MyApp.bin cp *.so MyApp.app/Contents/MacOS/ # Do NOT copy *.import.so - they are not needed at runtime </enscript> Installing eggs directly into the bundle: <enscript highlight="sh"> CHICKEN_REPOSITORY_PATH=MyApp.app/Contents/MacOS \ CHICKEN_INSTALL_REPOSITORY=MyApp.app/Contents/MacOS \ chicken-install medea http-client ... # Clean up compile-time only files rm -f MyApp.app/Contents/MacOS/*.import.so rm -f MyApp.app/Contents/MacOS/*.link MyApp.app/Contents/MacOS/*.o rm -f MyApp.app/Contents/MacOS/*.egg-info MyApp.app/Contents/MacOS/*.types rm -f MyApp.app/Contents/MacOS/types.db </enscript> === Self-Contained Distribution For distribution to machines without Homebrew, dylibs must be embedded. First, identify and copy all Homebrew dylibs (recursively for dependencies): <enscript highlight="sh"> mkdir -p MyApp.app/Contents/Frameworks # Copy direct dependencies for lib in $(otool -L MyApp.app/Contents/MacOS/MyApp.bin MyApp.app/Contents/MacOS/*.so 2>/dev/null \ | grep '/opt/homebrew\|/usr/local' | awk '{print $1}' | sort -u); do cp -n "$lib" MyApp.app/Contents/Frameworks/ 2>/dev/null || true done # Recursively copy dependencies of copied dylibs (repeat a few times) for i in 1 2 3 4 5; do for lib in MyApp.app/Contents/Frameworks/*.dylib; do for dep in $(otool -L "$lib" 2>/dev/null | grep '/opt/homebrew\|/usr/local' | awk '{print $1}'); do cp -n "$dep" MyApp.app/Contents/Frameworks/ 2>/dev/null || true done done done </enscript> Then rewrite the load paths using {{install_name_tool}}: <enscript highlight="sh"> # For dylibs in Frameworks - use -change, add @rpath for lib in MyApp.app/Contents/Frameworks/*.dylib; do for dep in $(otool -L "$lib" | grep '/opt/homebrew\|/usr/local' | awk '{print $1}'); do depname=$(basename "$dep") install_name_tool -change "$dep" "@rpath/$depname" "$lib" 2>/dev/null || true done install_name_tool -add_rpath "@loader_path" "$lib" 2>/dev/null || true done # For .so modules for so in MyApp.app/Contents/MacOS/*.so; do for dep in $(otool -L "$so" 2>/dev/null | grep '/opt/homebrew\|/usr/local' | awk '{print $1}'); do depname=$(basename "$dep") install_name_tool -change "$dep" "@rpath/$depname" "$so" 2>/dev/null || true done install_name_tool -add_rpath "@executable_path/../Frameworks" "$so" 2>/dev/null || true done # For main binary for dep in $(otool -L MyApp.app/Contents/MacOS/MyApp.bin | grep '/opt/homebrew\|/usr/local' | awk '{print $1}'); do depname=$(basename "$dep") install_name_tool -change "$dep" "@rpath/$depname" MyApp.app/Contents/MacOS/MyApp.bin 2>/dev/null || true done install_name_tool -add_rpath "@executable_path/../Frameworks" MyApp.app/Contents/MacOS/MyApp.bin 2>/dev/null || true </enscript> '''Critical:''' '''Never''' use {{install_name_tool -id}} on any binary. On x86_64 Macs, this corrupts CHICKEN-compiled binaries and dylibs. Only use {{-change}} to rewrite paths and {{-add_rpath}} to add search paths. ==== Launcher Binary A small C launcher is needed to set {{CHICKEN_REPOSITORY_PATH}} so CHICKEN can find the {{.so}} modules: <enscript highlight="c"> // launcher.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <libgen.h> #include <mach-o/dyld.h> int main(int argc, char *argv[]) { char exec_path[4096]; uint32_t size = sizeof(exec_path); if (_NSGetExecutablePath(exec_path, &size) != 0) return 1; char *dir = dirname(exec_path); char real_binary[4096]; snprintf(real_binary, sizeof(real_binary), "%s/MyApp.bin", dir); // CHICKEN needs this to find .so modules setenv("CHICKEN_REPOSITORY_PATH", dir, 1); // Note: DYLD_LIBRARY_PATH is NOT needed when using @rpath properly argv[0] = real_binary; execv(real_binary, argv); return 1; } </enscript> Compile and install the launcher: <enscript highlight="sh"> clang -mmacosx-version-min=13.0 -o launcher launcher.c mv MyApp.app/Contents/MacOS/MyApp MyApp.app/Contents/MacOS/MyApp.bin cp launcher MyApp.app/Contents/MacOS/MyApp </enscript> === 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 # '''No {{.import.so}} files needed:''' Import libraries are compile-time only, exclude from bundle # '''Never use {{install_name_tool -id}}:''' Corrupts binaries on x86_64; use {{-change}} and {{-add_rpath}} only # '''{{@rpath}} over {{DYLD_LIBRARY_PATH}}:''' Properly configured rpaths work without environment variables # '''Launcher for {{CHICKEN_REPOSITORY_PATH}}:''' Small C wrapper to set the module search path Hope this helps anyone else trying to build distributable macOS apps with CHICKEN Scheme!
Description of your changes:
I would like to authenticate
Authentication
Username:
Password:
Spam control
What do you get when you subtract 13 from 10?