Add make macos target: native jemacs-qt binary for macOS
ober
0d88a51333fe6e63752012b278a07de595d979d1
--- a/Makefile +++ b/Makefile @@ -41,13 +41,16 @@ export CHEZ_PCRE2_LIB := $(HOME)/mine/chez-pcre2 export JSH_FFI_LIB := $(HOME)/mine/jerboa-shell export CHEZ_QT_LIB := . export CHEZ_QT_SHIM_DIR := . +ifeq ($(UNAME_S),Darwin) + export CHEZ_DIR := $(shell ls -d /opt/homebrew/lib/csv*/tarm64osx 2>/dev/null | head -1) +endif .PHONY: all build rebuild run test-tier0 test-tier2 test-tier3 test-tier4 test-tier5 test-org test-extra test clean clean-generated \ test-org-duration test-org-element test-org-fold test-org-footnote \ test-org-lint test-org-num test-org-property test-org-src test-org-tempo \ test-vtscreen test-debug-repl test-qt test-qt-e2e build-qt binary binary-qt \ test-pty test-emacs test-functional test-term-hang \ - docker-deps static-qt clean-docker check-root build-jemacs-qt-static + docker-deps static-qt clean-docker check-root build-jemacs-qt-static macos all: build test @@ -152,6 +155,12 @@ libqt_shim.$(SHLIB_EXT): vendor/qt_shim.cpp qt_chez_shim.$(SHLIB_EXT): vendor/qt_chez_shim.c vendor/qt_shim.h gcc $(SHLIB_FLAGS) -O2 -o qt_chez_shim.$(SHLIB_EXT) vendor/qt_chez_shim.c -Ivendor -DQT_SCINTILLA_AVAILABLE -Wall +# macOS native binary — compile all modules + link jemacs-qt binary +macos: build libqt_shim.$(SHLIB_EXT) qt_chez_shim.$(SHLIB_EXT) repl_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) + cd $(HOME)/mine/chez-pcre2 && make + JSH_DIR=$(JSH) $(PRELOAD_ENV) $(SCHEME) $(LIBDIRS) --script build-binary-qt.ss + ./jemacs-qt --version + run-qt: build repl_shim.$(SHLIB_EXT) libqt_shim.$(SHLIB_EXT) vterm_shim.$(SHLIB_EXT) qt_chez_shim.$(SHLIB_EXT) $(PRELOAD_ENV) $(SCHEME) $(LIBDIRS) --script qt-main.ss --- a/build-binary-qt.ss +++ b/build-binary-qt.ss @@ -92,6 +92,13 @@ (let ((v (getenv "JEMACS_STATIC"))) (and v (not (string=? v "0")) (not (string=? v ""))))) +;; Platform detection +(define macos-build? + (let ((mt (symbol->string (machine-type)))) + (and (>= (string-length mt) 3) + (string=? (substring mt (- (string-length mt) 3) (string-length mt)) "osx")))) +(define shlib-ext (if macos-build? "dylib" "so")) + ;; Check critical dependencies ;; For static builds, skip checks on .o files that this script compiles in step 5 ;; (jemacs-qt-chez-shim.o, pcre2_shim.o). Only check pre-existing external deps. @@ -101,15 +108,18 @@ (printf "Error: ~a not found at ~a~n" label path) (exit 1))) (if jemacs-static? + ;; Static: need pre-compiled jerboa core and libqt_shim.a (list (format "~a/jerboa/core.so" jerboa-dir) (format "~a/libqt_shim.a" qt-shim-dir)) - (list (format "~a/jerboa/core.so" jerboa-dir) - "qt_chez_shim.so" - (format "~a/pcre2_shim.so" pcre2-dir) - (format "~a/libqt_shim.so" qt-shim-dir))) + ;; Dynamic: only check shim files — Scheme libs are compiled by step 1 + (list (format "qt_chez_shim.~a" shlib-ext) + (format "~a/pcre2_shim.~a" pcre2-dir shlib-ext) + (format "libqt_shim.~a" shlib-ext))) (if jemacs-static? (list "jerboa core.so" "libqt_shim.a") - (list "jerboa core.so" "qt_chez_shim.so" "pcre2_shim.so" "libqt_shim.so"))) + (list (format "qt_chez_shim.~a" shlib-ext) + (format "pcre2_shim.~a" shlib-ext) + (format "libqt_shim.~a" shlib-ext)))) (printf "Chez dir: ~a~n" chez-dir) (printf "Jerboa dir: ~a~n" jerboa-dir) @@ -377,12 +387,15 @@ (unless (= 0 (system cmd)) (display "Error: qt_chez_shim compilation failed\n") (exit 1))) - ;; Shared .so for dynamic builds + ;; Shared library for dynamic builds (platform-appropriate flags + extension) (unless jemacs-static? - (let ((cmd (format "gcc -shared -fPIC -O2 -DQT_SCINTILLA_AVAILABLE -o qt_chez_shim.so vendor/qt_chez_shim.c -I~a ~a -Wall 2>&1" - qt-shim-dir qt-cflags))) + (let* ((shlib-flags (if macos-build? + "-dynamiclib -Wl,-undefined,dynamic_lookup" + "-shared -fPIC")) + (cmd (format "gcc ~a -O2 -DQT_SCINTILLA_AVAILABLE -o qt_chez_shim.~a vendor/qt_chez_shim.c -I~a ~a -Wall 2>&1" + shlib-flags shlib-ext qt-shim-dir qt-cflags))) (unless (= 0 (system cmd)) - (display "Error: qt_chez_shim.so compilation failed\n") + (display "Error: qt_chez_shim shlib compilation failed\n") (exit 1))))) ;; Static: generate + compile foreign symbol registration table @@ -446,9 +459,9 @@ echo OK" (display "Error: qt_static_symbols.c compilation failed\n") (exit 1))))) -;; jsh FFI shim (needed for static builds so ffi_* symbols are in the binary) +;; jsh FFI shim (needed for static builds and macOS dynamic builds so ffi_* symbols are in the binary) ;; ffi-shim.c lives in the jsh root (parent of jsh-dir which is the src/ subdir) -(when jemacs-static? +(when (or jemacs-static? macos-build?) (let* ((jsh-root (path-parent jsh-dir)) (cmd (format "gcc -c -O2 -o jemacs-qt-jsh-ffi.o ~a/ffi-shim.c -Wall 2>&1" jsh-root))) @@ -457,7 +470,7 @@ echo OK" (exit 1)))) ;; jsh embed-crypto (pure C crypto for embed encryption — no external deps) -(when jemacs-static? +(when (or jemacs-static? macos-build?) (let* ((jsh-root (path-parent jsh-dir)) (cmd (format "gcc -c -O2 -o jemacs-qt-embed-crypto.o ~a/embed-crypto.c -I~a -Wall 2>&1" jsh-root jsh-root))) @@ -466,7 +479,7 @@ echo OK" (exit 1)))) ;; jsh ssh-agent stub (chez_ssh_agent_stop referenced in main.sls but not needed for jemacs) -(when jemacs-static? +(when (or jemacs-static? macos-build?) (let ((stub-file "jemacs-qt-ssh-agent-stub.c")) (call-with-output-file stub-file (lambda (out) @@ -517,6 +530,88 @@ echo OK" (display "Error: landlock-shim.c compilation failed\n") (exit 1)))) +;; macOS: generate symbol registration table. +;; Two-part: (1) custom symbols from .o files via nm (exact, no undefined refs) +;; (2) system/POSIX symbols via dlsym(RTLD_DEFAULT, name) at runtime +;; (covers open, close, fork, etc. that live in libSystem.dylib) +(when (and macos-build? (not jemacs-static?)) + (let* ((include-dir chez-dir) + ;; Part 1: extract custom symbol names from linked .o files + (nm-cmd + "nm -gU jemacs-qt-jsh-ffi.o jemacs-qt-embed-crypto.o jemacs-qt-ssh-agent-stub.o jemacs-qt-chez-shim.o 2>/dev/null | \ +awk '$2 == \"T\" || $2 == \"t\" { print $NF }' | sed 's/^_//' | sort -u | \ +grep -v '^$' | grep -v '^register_static_foreign_symbols$'") + (custom-syms + (let* ((tmpf "/tmp/macos_nm_syms.txt") + (_ (system (format "~a > ~a 2>/dev/null" nm-cmd tmpf))) + (p (if (file-exists? tmpf) (open-input-file tmpf) #f)) + (lines (if p + (let loop ((acc '())) + (let ((line (get-line p))) + (if (eof-object? line) + (begin (close-port p) (delete-file tmpf) (reverse acc)) + (loop (if (> (string-length line) 0) + (cons line acc) acc))))) + '()))) + lines)) + ;; Part 2: POSIX/system symbol names to register via RTLD_DEFAULT + (system-syms + '("_exit" "__error" + "accept" "access" "bind" "chdir" "chmod" "chown" "chroot" + "close" "connect" "dup" "dup2" "execve" "execvp" + "fcntl" "fdopen" "flock" "fork" + "freeaddrinfo" "fstat" "ftruncate" + "getaddrinfo" "getegid" "geteuid" "getgid" "getgrnam" + "getpagesize" "getpgid" "getpid" "getppid" + "getpwnam" "getrlimit" "getsockname" "getuid" + "htons" "inet_ntop" "inet_pton" "ioctl" "isatty" + "kevent" "kill" "kqueue" "listen" "localtime" + "lseek" "lstat" "madvise" "mkdir" "mkdtemp" + "mkfifo" "mkstemp" "mmap" "msync" "munmap" + "ntohs" "open" "pipe" "read" "readlink" "realpath" + "recvfrom" "rmdir" "sandbox_init" "sandbox_free_error" + "sendto" "setegid" "setenv" "seteuid" "setgid" + "setpgid" "setrlimit" "setsid" "setsockopt" "setuid" + "sigaddset" "sigdelset" "sigemptyset" "sigfillset" + "sigismember" "sigprocmask" "sigwait" + "socket" "stat" "strerror" "strftime" + "syscall" "sysconf" "tcgetattr" "tcgetpgrp" + "tcsetattr" "tcsetpgrp" + "umask" "unlink" "unsetenv" "waitpid" "write"))) + ;; Write qt_static_symbols.c + (call-with-output-file "qt_static_symbols.c" + (lambda (out) + (fprintf out "/* Auto-generated (macOS) — do not edit */\n") + (fprintf out "#include \"scheme.h\"\n") + (fprintf out "#include <dlfcn.h>\n\n") + ;; Forward declare custom symbols + (for-each (lambda (sym) + (fprintf out "extern void ~a(void);\n" sym)) + custom-syms) + (fprintf out "\nvoid register_static_foreign_symbols(void) {\n") + ;; Register custom symbols + (for-each (lambda (sym) + (fprintf out " Sforeign_symbol(\"~a\", (void*)~a);\n" sym sym)) + custom-syms) + ;; Register system symbols via RTLD_DEFAULT + (fprintf out " /* System/POSIX symbols via RTLD_DEFAULT */\n") + (fprintf out " { void *_s;\n") + (for-each (lambda (sym) + (fprintf out " if ((_s = dlsym(RTLD_DEFAULT, \"~a\"))) Sforeign_symbol(\"~a\", _s);\n" + sym sym)) + system-syms) + (fprintf out " }\n") + (fprintf out "}\n")) + 'replace) + (let ((total (+ (length custom-syms) (length system-syms)))) + (printf " Generated qt_static_symbols.c with ~a symbol registrations (~a custom + ~a system)~n" + total (length custom-syms) (length system-syms))) + (let* ((cmd (format "gcc -c -O2 -o qt_static_symbols.o qt_static_symbols.c -I~a -Wall 2>&1" + include-dir))) + (unless (= 0 (system cmd)) + (display "Error: qt_static_symbols.c (macOS) compilation failed\n") + (exit 1))))) + ;; jemacs-qt-main.c (let* ((static-flag (if jemacs-static? "-DJEMACS_STATIC_BUILD" "")) (cmd (format "gcc -c -O2 ~a -o jemacs-qt-main.o jemacs-qt-main.c -I~a -I. -Wall 2>&1" @@ -570,12 +665,37 @@ qt_static_symbols.o \ (display "Error: Static link failed\n") (exit 1))) ;; ─── Dynamic link (default local build) ──────────────────────────────── - (let* ((pcre2-libs (shell-output "pkg-config --libs libpcre2-8" "-lpcre2-8")) + (let* ((macos? (string=? "Darwin" (shell-output "uname -s" "Linux"))) + (pcre2-libs (shell-output "pkg-config --libs libpcre2-8" "-lpcre2-8")) (qt-libs (shell-output "pkg-config --libs Qt6Widgets" "-lQt6Widgets -lQt6Gui -lQt6Core")) - ;; Link against ./libqt_shim.so (local copy with JEMACS_CHEZ_SMP) - ;; rather than qt-shim-dir (gerbil-qt vendor — no Sdeactivate) - (cmd (format "g++ -rdynamic -o jemacs-qt jemacs-qt-main.o jemacs-qt-chez-shim.o jemacs-qt-pcre2-shim.o ~a ~a -L~a -lkernel -llz4 -lz -lm -ldl -lpthread -luuid -lncurses -lstdc++ -L. -lqt_shim -lqscintilla2_qt6 -lvterm -Wl,-rpath,~a -Wl,-rpath,'$ORIGIN' 2>&1" - pcre2-libs qt-libs chez-dir chez-dir))) + (qsci-dir (shell-output "brew --prefix qscintilla2 2>/dev/null" "")) + (vterm-dir (shell-output "brew --prefix libvterm 2>/dev/null" "")) + (ncurses-dir (shell-output "brew --prefix ncurses 2>/dev/null" "")) + ;; Link against ./libqt_shim.dylib/.so (local copy with JEMACS_CHEZ_SMP) + (cmd + (if macos? + ;; macOS: frameworks, no -rdynamic/-uuid, use @executable_path rpath + (format "g++ -o jemacs-qt \ +jemacs-qt-main.o jemacs-qt-chez-shim.o jemacs-qt-pcre2-shim.o jemacs-qt-jsh-ffi.o jemacs-qt-embed-crypto.o jemacs-qt-ssh-agent-stub.o qt_static_symbols.o \ +~a ~a \ +-L~a -lkernel -llz4 -lz -lm -ldl -lpthread -liconv -lstdc++ \ +-L. -lqt_shim \ +-L~a/lib -lqscintilla2_qt6 \ +-L~a/lib -lvterm \ +-L~a/lib -lncurses \ +-Wl,-rpath,~a -Wl,-rpath,@executable_path 2>&1" + pcre2-libs qt-libs + chez-dir + qsci-dir vterm-dir ncurses-dir + chez-dir) + ;; Linux: standard dynamic link + (format "g++ -rdynamic -o jemacs-qt \ +jemacs-qt-main.o jemacs-qt-chez-shim.o jemacs-qt-pcre2-shim.o \ +~a ~a \ +-L~a -lkernel -llz4 -lz -lm -ldl -lpthread -luuid -lncurses -lstdc++ \ +-L. -lqt_shim -lqscintilla2_qt6 -lvterm \ +-Wl,-rpath,~a -Wl,-rpath,'$ORIGIN' 2>&1" + pcre2-libs qt-libs chez-dir chez-dir)))) (printf " ~a~n" cmd) (unless (= 0 (system cmd)) (display "Error: Link failed\n") @@ -611,15 +731,14 @@ qt_static_symbols.o \ (printf "~nInstall:~n") (printf " cp jemacs-qt /usr/local/bin/~n")) (begin - ;; Copy shim .so files alongside binary for dynamic builds - (system (format "cp ~a/libqt_shim.so . 2>/dev/null; true" qt-shim-dir)) - ;; qt_chez_shim.so already built locally from vendor/qt_chez_shim.c - (system (format "cp ~a/pcre2_shim.so . 2>/dev/null; true" pcre2-dir)) + ;; Copy shim files alongside binary for dynamic builds + ;; (libqt_shim is already built locally; pcre2_shim may need copying) + (system (format "cp ~a/pcre2_shim.~a . 2>/dev/null; true" pcre2-dir shlib-ext)) (printf "~nBundle (keep these together):~n") (printf " ./jemacs-qt~n") - (printf " ./libqt_shim.so~n") - (printf " ./qt_chez_shim.so~n") - (printf " ./pcre2_shim.so~n") + (printf " ./libqt_shim.~a~n" shlib-ext) + (printf " ./qt_chez_shim.~a~n" shlib-ext) + (printf " ./pcre2_shim.~a~n" shlib-ext) (printf " ./vterm_shim.so~n") (printf "~nRun:~n") (printf " ./jemacs-qt # launch Qt editor~n") --- a/jemacs-qt-main.c +++ b/jemacs-qt-main.c @@ -1,30 +1,27 @@ /* * jemacs-qt-main.c — Custom entry point for jemacs-qt (jerboa-emacs Qt frontend). * - * Same threading workaround as jemacs-main.c: * - Boot file contains only libraries (no program) - * - Program is loaded via Sscheme_script on a memfd + * - Program is loaded via Sscheme_script from an in-memory file + * (memfd on Linux, temp file on macOS) * - * The Qt event loop runs on its own pthread (created by qt-app-create in - * qt_shim.cpp). That pthread registers via Sactivate_thread/Sdeactivate_thread - * (implemented in qt_chez_shim.c) before invoking Chez foreign-callable - * trampolines. No special handling is needed here. - * - * The CHEZ_QT_LIB env var points qt_chez_shim to where qt_chez_shim.so - * lives (for load-shared-object). We point it at the binary's own directory - * since qt_chez_shim is compiled into the binary itself — but we still need - * the libqt_shim.so (gerbil-qt vendor shim) to be loadable. - * Set CHEZ_QT_SHIM_DIR to where libqt_shim.so lives (same dir as binary, - * or let the rpath handle it). + * On macOS, Qt runs on the main thread (Cocoa requirement). The event loop + * is driven by repeated processEvents() calls from the Scheme polling loop. */ +#ifdef __APPLE__ +#include <mach-o/dyld.h> +#else #define _GNU_SOURCE +#include <sys/mman.h> +#endif + #include <stdlib.h> #include <string.h> #include <stdio.h> #include <unistd.h> #include <libgen.h> -#include <sys/mman.h> +#include <fcntl.h> #include "scheme.h" #include "jemacs_qt_program.h" /* jemacs_qt_program_data[], jemacs_qt_program_size */ #include "jemacs_qt_petite_boot.h" /* petite_boot_data[], petite_boot_size */ @@ -35,16 +32,27 @@ int main(int argc, char *argv[]) { /* Resolve real executable path */ char exe_buf[4096]; char exe_dir[4096]; + int got_exe = 0; + +#ifdef __APPLE__ + uint32_t exe_size = sizeof(exe_buf); + if (_NSGetExecutablePath(exe_buf, &exe_size) == 0) { + got_exe = 1; + } +#else ssize_t len = readlink("/proc/self/exe", exe_buf, sizeof(exe_buf) - 1); if (len > 0) { exe_buf[len] = '\0'; + got_exe = 1; + } +#endif + + if (got_exe) { setenv("JEMACS_EXE", exe_buf, 1); strncpy(exe_dir, exe_buf, sizeof(exe_dir) - 1); exe_dir[sizeof(exe_dir) - 1] = '\0'; char *dir = dirname(exe_dir); - /* Point FFI shim loaders at the binary's directory. - * The shims (qt_chez_shim, pcre2_shim) are compiled into the binary, - * and libqt_shim.so must be alongside the binary (rpath or LD_LIBRARY_PATH). */ + /* Point FFI shim loaders at the binary's directory */ if (!getenv("CHEZ_QT_LIB")) setenv("CHEZ_QT_LIB", dir, 0); if (!getenv("CHEZ_QT_SHIM_DIR")) @@ -55,20 +63,52 @@ int main(int argc, char *argv[]) { setenv("CHEZ_SCINTILLA_LIB", dir, 0); } - /* Create memfd for embedded program .so */ - int fd = memfd_create("jemacs-qt-program", MFD_CLOEXEC); + /* Write embedded program .so to a file Chez can dlopen */ + char prog_path[256]; + int fd; + +#ifdef __APPLE__ + /* macOS: no memfd — write to a temp file */ + snprintf(prog_path, sizeof(prog_path), "/tmp/jemacs-qt-program.XXXXXX"); + fd = mkstemp(prog_path); + if (fd < 0) { + perror("mkstemp"); + return 1; + } + /* Rename to add .so extension so dlopen recognises it as a shared lib */ + char prog_path_so[256]; + snprintf(prog_path_so, sizeof(prog_path_so), "%s.so", prog_path); + if (rename(prog_path, prog_path_so) != 0) { + perror("rename"); + close(fd); + return 1; + } + close(fd); + fd = open(prog_path_so, O_WRONLY | O_CREAT | O_TRUNC, 0700); + if (fd < 0) { + perror("open prog_path_so"); + return 1; + } + strncpy(prog_path, prog_path_so, sizeof(prog_path) - 1); +#else + /* Linux: memfd — anonymous in-memory file */ + fd = memfd_create("jemacs-qt-program", MFD_CLOEXEC); if (fd < 0) { perror("memfd_create"); return 1; } + snprintf(prog_path, sizeof(prog_path), "/proc/self/fd/%d", fd); +#endif + if (write(fd, jemacs_qt_program_data, jemacs_qt_program_size) != (ssize_t)jemacs_qt_program_size) { - perror("write memfd"); + perror("write prog"); close(fd); return 1; } - char prog_path[64]; - snprintf(prog_path, sizeof(prog_path), "/proc/self/fd/%d", fd); +#ifdef __APPLE__ + close(fd); /* file is on disk — close write fd before dlopen */ +#endif /* Initialize Chez Scheme */ Sscheme_init(NULL); @@ -79,30 +119,24 @@ int main(int argc, char *argv[]) { Sregister_boot_file_bytes("jemacs-qt", (void*)jemacs_qt_boot_data, jemacs_qt_boot_size); #ifdef JEMACS_STATIC_BUILD - /* Tell Scheme libraries they are in a static build so they skip - * load-shared-object calls (dlopen("file.so") fails in musl static). - * Must be set before Sbuild_heap so library bodies see it. */ setenv("JEMACS_STATIC", "1", 1); #endif /* Build heap from libraries only — no program */ Sbuild_heap(NULL, NULL); -#ifdef JEMACS_STATIC_BUILD - /* Register all FFI symbols after heap is built. - * Sforeign_symbol requires an initialized Scheme heap. - * foreign-procedure in library bodies uses dlsym(RTLD_DEFAULT) first; - * this call supplements that for any symbols missed by dlsym. */ +#if defined(JEMACS_STATIC_BUILD) || defined(__APPLE__) extern void register_static_foreign_symbols(void); register_static_foreign_symbols(); #endif - /* Run via Sscheme_script so fork-thread works. - * Pass full argv so argv[0]=binary name and argv[1..]=user args. - * (command-line-arguments) returns ("./jemacs-qt" args...) — member checks work. */ int status = Sscheme_script(prog_path, argc, (const char **)argv); +#ifdef __APPLE__ + unlink(prog_path); /* clean up temp file */ +#else close(fd); +#endif Sscheme_deinit(); return status; }