I forgot to report back :)
I'm not sure the process is the best, but given the different constraints
(multiple custom FFI bindings for C++ and ObjC++ libraries/frameworks), I
think the process kind of works. I also needed x86_64 AND arm64 builds. I
could not get a lipo version combined, so I'm shipping both separately,
good enough for now :)
I also created a bunch of modules that might be useful to the community.
I'll try to spend some time extracting them into eggs (e.g. imgui.scm,
umami.scm for umami analytics, llm.scm for a simple chat-like interface
with tool usage, a couple others).
Anyway, here's the full writeup.
Building a macOS App Bundle with CHICKEN Scheme
================================================
I wanted to share how I'm building a macOS app bundle for my CHICKEN Scheme
project. The app combines native C++/Objective-C code (FFmpeg, ImGui, native
dialogs) with Scheme modules.
Overview
--------
The architecture 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
1. 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 $@
2. 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 $@
3. 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
4. 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 ...
5. 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!
All the best,
Rolando
On Thu, Dec 25, 2025 at 6:39 AM Rolando Abarca <[email protected]>
wrote:
> Thank you so much Felix, and merry xmas to you and all the CHICKEN family!
>
> I'll try your suggestions and report back.
>
> All the best,
> Rolando
>
> On Thu, Dec 25, 2025 at 10:48 AM Felix Winkelmann <
> [email protected]> wrote:
>
>> On Wed Dec 24, 2025 at 10:30 PM CET, Rolando Abarca via Chicken-users
>> wrote:
>> >
>> > I'm trying to deploy a CHICKEN application as a self-contained macOS app
>> > bundle, but I'm running into issues with module loading at runtime. I'd
>> > appreciate any guidance on the correct approach.
>>
>> Hi, Rolando! Merry christmas to you!
>>
>> > With -uses, the module code is statically linked into the binary and the
>> > toplevel runs at startup. However, when another module does (import
>> > mymodule), CHICKEN's runtime still calls load-extension looking for
>> > mymodule.so, which doesn't exist because the code is embedded in the
>> binary.
>> >
>> > 1. What is the correct way to deploy a CHICKEN application with custom
>> > modules on macOS?
>>
>> To get things running, I would avoid using units and such and start with
>> the simplest configuration: just compile everying to .so's (including the
>> import libraries) and ad them to the bundle. Use the "-deployed" option
>> for the main program to make sure the runtime linker path is added
>> with "install_name_tool(1)" (you can use the "-v" option to csc to see
>> the commands executed). You should also use -private-repository to
>> ensure eggs and modules are taken from the bundle.
>>
>> Modules must be "registered" to access their executable code, this
>> is normally done via import libs, but can be compiled in, usually
>> when doing static linking. This can all be optimized, but I suggest
>> you start with the described process and try to tweak things later.
>>
>> It's been a while since I looked at Mac deployment but it is definitely
>> doable, keep me posted about your progress.
>>
>> cheers,
>> felix
>>
>>