From c495d1fbe20d034a61d31898a138e0cf808b8122 Mon Sep 17 00:00:00 2001 From: Thomas Gazagnaire Date: Mon, 29 Jun 2026 14:31:02 -0700 Subject: [PATCH 1/3] nolibc: format printf floats as double, not long double On targets where long double is IEEE quad (aarch64), vfprintf's `union arg` held the float value as long double, so `%f` (and the fmt_fp call) round-tripped double -> long double -> double through __extenddftf2/__trunctfdf2. That pulls libgcc (or compiler-rt) soft-float into every freestanding link that uses printf, even for integer-only formats. fmt_fp already takes a double, so the long double was pure overhead. Use double for the field; %L float conversions are treated as double (never used by OCaml unikernels). aarch64 cross unikernels now link with no libgcc at all (clang already inlines the atomics). --- nolibc/vfprintf.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/nolibc/vfprintf.c b/nolibc/vfprintf.c index 9e886a0b..f852e9b8 100644 --- a/nolibc/vfprintf.c +++ b/nolibc/vfprintf.c @@ -121,7 +121,10 @@ static const unsigned char states[]['z'-'A'+1] = { union arg { uintmax_t i; - long double f; + /* double, not long double: avoids pulling libgcc soft-float (__extenddftf2, + __trunctfdf2) into freestanding links; %L is treated as double (fmt_fp + formats at double). */ + double f; void *p; }; @@ -151,7 +154,7 @@ static void pop_arg(union arg *arg, int type, va_list *ap) break; case UIPTR: arg->i = (uintptr_t)va_arg(*ap, void *); #endif break; case DBL: arg->f = va_arg(*ap, double); - break; case LDBL: arg->f = va_arg(*ap, long double); + break; case LDBL: arg->f = va_arg(*ap, double); } } From ac1fb9e3e04c6715de9ce91acb36baa38f7f99a8 Mon Sep 17 00:00:00 2001 From: Thomas Gazagnaire Date: Wed, 8 Jul 2026 12:33:10 -0700 Subject: [PATCH 2/3] toolchain: drop -lgcc, inline aarch64 atomics The aarch64 wrapper linked -lgcc for printf's long-double soft-float (now gone: printf formats as double) and for GCC's default outline atomics (-moutline-atomics lowers C11 atomics to __aarch64_* helpers in libgcc, which also pull __getauxval, absent on a freestanding solo5 target). Pass -mno-outline-atomics so atomics inline (LL/SC); the link then needs no compiler runtime, so drop -lgcc and let a pure-LLVM toolchain link aarch64 unikernels. --- gen_toolchain_tool.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gen_toolchain_tool.sh b/gen_toolchain_tool.sh index e711ad30..4ec13f81 100755 --- a/gen_toolchain_tool.sh +++ b/gen_toolchain_tool.sh @@ -14,11 +14,12 @@ gen_cc() { CFLAGS="$TOOL_CFLAGS" LDFLAGS="$TOOL_LDFLAGS" - EXTRALIBS="" + # GCC lowers aarch64 C11 atomics to libgcc __aarch64_* outline helpers by + # default; inline them so the freestanding link needs no compiler runtime. case "$ARCH" in aarch64) - EXTRALIBS="-lgcc" + CFLAGS="$CFLAGS -mno-outline-atomics" ;; esac @@ -60,7 +61,6 @@ if [ -z "\$compiling" ]; then -Wl,--start-group \\ -lnolibc \\ -lopenlibm \\ - $EXTRALIBS \\ -Wl,--end-group fi From 69e8eb309b5543e67da738ef30dc034eb392d876 Mon Sep 17 00:00:00 2001 From: Thomas Gazagnaire Date: Wed, 8 Jul 2026 12:38:04 -0700 Subject: [PATCH 3/3] support building the cross-compiler on macOS Accept a macOS/arm64 host. The OCaml cross build uses the llvm- toolprefix, and --toolchain-use-absolute-path pins the keg-only Homebrew llvm tools (off PATH when the cross compiler is later used) to absolute paths; other hosts keep bare, relocatable tool names. Build the nolibc/openlibm archives with the toolprefix ar/ranlib (Apple's cannot index ELF), and suppress -Wasm-operand-widths in nolibc (openlibm's aarch64 fenv asm trips clang under -Werror). Patch Makefile.cross so crossopt does not pass -function-sections to the host .opt tools (the macOS host ocamlopt is Mach-O and rejects it), keeping it for the target library so the linker can still garbage-collect unused code. Add a macOS CI job building the cross-compiler. --- .github/workflows/test.yml | 18 ++++++++ Makefile | 6 +++ configure.sh | 23 ++++++++++ gen_toolchain_tool.sh | 14 +++++-- ocaml-solo5-cross-aarch64.opam | 7 +++- ...keep-function-sections-off-the-host-.patch | 42 +++++++++++++++++++ ...keep-function-sections-off-the-host-.patch | 42 +++++++++++++++++++ 7 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 patches/5.4.1/0004-Makefile.cross-keep-function-sections-off-the-host-.patch create mode 100644 patches/5.5.0/0004-Makefile.cross-keep-function-sections-off-the-host-.patch diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 40c2dccf..5989ccb8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,3 +61,21 @@ jobs: - name: Compile examples with xen run: MODE=xen opam exec -- dune build --root example --display short if: matrix.runner == 'ubuntu-latest' + + macos: + name: macOS aarch64 cross-compiler + runs-on: macos-latest + env: + OPAMYES: 1 + steps: + - uses: actions/checkout@v7 + - uses: ocaml/setup-ocaml@v3 + with: + ocaml-compiler: 5.4.1 + opam-pin: false + - name: Build the cross-compiler + run: | + opam pin add -n solo5 git+https://github.com/samoht/solo5.git#macos + opam pin add -n solo5-cross-aarch64 git+https://github.com/samoht/solo5.git#macos + opam pin add -n ocaml-solo5-cross-aarch64 . + opam install ocaml-solo5-cross-aarch64 diff --git a/Makefile b/Makefile index add901bc..f72883c2 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,8 @@ nolibc/libnolibc.a: phony-nolibc phony-nolibc: $(MAKE) -C nolibc libnolibc.a \ "CC=$(MAKECONF_TOOLCHAIN)-cc" \ + "AR=$(MAKECONF_TOOLPREFIX)ar" \ + "RANLIB=$(MAKECONF_TOOLPREFIX)ranlib" \ "FREESTANDING_CFLAGS=$(NOLIBC_CFLAGS)" # OPENLIBM @@ -38,6 +40,8 @@ openlibm/libopenlibm.a: phony-openlibm phony-openlibm: $(MAKE) -C openlibm libopenlibm.a \ "CC=$(MAKECONF_TOOLCHAIN)-cc" \ + "AR=$(MAKECONF_TOOLPREFIX)ar" \ + "RANLIB=$(MAKECONF_TOOLPREFIX)ranlib" \ "CPPFLAGS=$(LIB_CFLAGS)" # TOOLCHAIN @@ -72,6 +76,7 @@ $(TOOLDIR_FOR_BUILD)/$(MAKECONF_TARGET_ARCH)-solo5-ocaml-%: \ ARCH="$(MAKECONF_TARGET_ARCH)" \ SOLO5_TOOLCHAIN="$(MAKECONF_TOOLCHAIN)" \ OTHERTOOLPREFIX="$(MAKECONF_TOOLPREFIX)" \ + TOOLCHAIN_ABSOLUTE_PATH="$(MAKECONF_TOOLCHAIN_ABSOLUTE_PATH)" \ TOOL_CFLAGS="$(TOOLCHAIN_BUILD_CFLAGS)" \ TOOL_LDFLAGS="$(TOOLCHAIN_BUILD_LDFLAGS)" \ sh $< $* > $@ @@ -82,6 +87,7 @@ $(TOOLDIR_FINAL)/$(MAKECONF_TARGET_ARCH)-solo5-ocaml-%: \ ARCH="$(MAKECONF_TARGET_ARCH)" \ SOLO5_TOOLCHAIN="$(MAKECONF_TOOLCHAIN)" \ OTHERTOOLPREFIX="$(MAKECONF_TOOLPREFIX)" \ + TOOLCHAIN_ABSOLUTE_PATH="$(MAKECONF_TOOLCHAIN_ABSOLUTE_PATH)" \ TOOL_CFLAGS="$(TOOLCHAIN_FINAL_CFLAGS)" \ TOOL_LDFLAGS="$(TOOLCHAIN_FINAL_LDFLAGS)" \ sh $< $* > $@ diff --git a/configure.sh b/configure.sh index f9dfd870..d7b5a06d 100755 --- a/configure.sh +++ b/configure.sh @@ -31,6 +31,10 @@ Options: (default: \`TARGET-cc -dumpmachine\`-). --ocaml-configure-option=OPTION Add an option to the OCaml compiler configuration. + --toolchain-use-absolute-path + Resolve the toolchain binaries to absolute paths, for hosts where they + are off PATH when the cross compiler is later used (e.g. Homebrew's + keg-only llvm tools on macOS). EOM exit 1 } @@ -57,6 +61,9 @@ while [ $# -gt 0 ]; do --ocaml-configure-option=*) OCAML_CONFIGURE_OPTIONS="${OCAML_CONFIGURE_OPTIONS} ${OPT#*=}" ;; + --toolchain-use-absolute-path) + TOOLCHAIN_ABSOLUTE_PATH=1 + ;; --help) usage ;; @@ -77,6 +84,21 @@ TARGET_TRIPLET="$("$CONFIG_TARGET-cc" -dumpmachine)" MAKECONF_TOOLPREFIX="${MAKECONF_TOOLPREFIX:-$TARGET_TRIPLET-}" +# With --toolchain-use-absolute-path, pin the prefix to where the tools live when +# they are off PATH (macOS: Homebrew's keg-only llvm). Only when the user asked. +if [ -n "${TOOLCHAIN_ABSOLUTE_PATH}" ] \ + && ! command -v "${MAKECONF_TOOLPREFIX}ar" >/dev/null 2>&1; then + for bindir in \ + "$(command -v brew >/dev/null 2>&1 && brew --prefix llvm 2>/dev/null)/bin" \ + /opt/homebrew/opt/llvm/bin /usr/local/opt/llvm/bin \ + /opt/local/libexec/llvm-*/bin; do + if [ -x "${bindir}/${MAKECONF_TOOLPREFIX}ar" ]; then + MAKECONF_TOOLPREFIX="${bindir}/${MAKECONF_TOOLPREFIX}" + break + fi + done +fi + case "${TARGET_TRIPLET}" in amd64-*|x86_64-*) TARGET_ARCH="x86_64" @@ -96,4 +118,5 @@ MAKECONF_TOOLCHAIN=${CONFIG_TARGET} MAKECONF_TOOLPREFIX=${MAKECONF_TOOLPREFIX} MAKECONF_TARGET_ARCH=${TARGET_ARCH} MAKECONF_OCAML_CONFIGURE_OPTIONS=${OCAML_CONFIGURE_OPTIONS} +MAKECONF_TOOLCHAIN_ABSOLUTE_PATH=${TOOLCHAIN_ABSOLUTE_PATH} EOM diff --git a/gen_toolchain_tool.sh b/gen_toolchain_tool.sh index 4ec13f81..18a97d09 100755 --- a/gen_toolchain_tool.sh +++ b/gen_toolchain_tool.sh @@ -8,6 +8,7 @@ # SOLO5_TOOLCHAIN: the target for the wrapped Solo5 toolchain # OTHERTOOLPREFIX: the prefix for tools not in the Solo5 toolchain # TARGET_X: overrides the command for binutil X +# TOOLCHAIN_ABSOLUTE_PATH: if set, resolve tools to absolute paths gen_cc() { # Note that -nostdlib is not required, as it is injected by Solo5' cc, ld @@ -69,6 +70,13 @@ exec "$SOLO5_TOOLCHAIN-cc" "\$@" EOF } +# With TOOLCHAIN_ABSOLUTE_PATH set, resolve to an absolute path, for tools that +# are off PATH when the cross compiler is later used (keg-only llvm on macOS); +# otherwise keep the bare name to stay relocatable. +resolve() { + if [ -n "$TOOLCHAIN_ABSOLUTE_PATH" ]; then command -v -- "$1"; else printf %s "$1"; fi +} + gen_tool() { TOOL="$1" case "$TOOL" in @@ -103,15 +111,15 @@ gen_tool() { if test "$TARGET_TOOL" ; then TOOL="$TARGET_TOOL" elif command -v -- "$SOLO5_TOOLCHAIN-$TOOL" > /dev/null; then - TOOL="$SOLO5_TOOLCHAIN-$TOOL" + TOOL="$(resolve "$SOLO5_TOOLCHAIN-$TOOL")" else case "$TOOL" in as) - TOOL="$SOLO5_TOOLCHAIN-cc -c" + TOOL="$(resolve "$SOLO5_TOOLCHAIN-cc") -c" ;; *) if command -v -- "$OTHERTOOLPREFIX$TOOL" > /dev/null; then - TOOL="$OTHERTOOLPREFIX$TOOL" + TOOL="$(resolve "$OTHERTOOLPREFIX$TOOL")" fi ;; esac diff --git a/ocaml-solo5-cross-aarch64.opam b/ocaml-solo5-cross-aarch64.opam index 7ebf9ee7..0253e459 100644 --- a/ocaml-solo5-cross-aarch64.opam +++ b/ocaml-solo5-cross-aarch64.opam @@ -13,6 +13,8 @@ build: [ "--target=aarch64-solo5-none-static" "--ocaml-configure-option=--disable-flat-float-array" {ocaml-option-no-flat-float-array:installed} "--ocaml-configure-option=--enable-flambda" {ocaml-option-flambda:installed} + "--othertoolprefix=llvm-" {os = "macos"} + "--toolchain-use-absolute-path" {os = "macos"} ] [make "-j%{jobs}%"] [make "%{name}%.install"] @@ -32,7 +34,7 @@ depends: [ "ocamlfind" {build} "ocaml-src" {build} "ocaml" {= "5.4.1" | = "5.5.0"} - "solo5" {>= "0.9.1"} + "solo5" {>= "0.9.1" & os != "macos"} # no native solo5 build on macOS "solo5-cross-aarch64" {>= "0.9.1" } ] conflicts: [ @@ -45,7 +47,8 @@ conflicts: [ available: [ ((os = "linux" & (arch = "x86_64" | arch = "arm64")) | (os = "freebsd" & arch = "x86_64") - | (os = "openbsd" & arch = "x86_64")) + | (os = "openbsd" & arch = "x86_64") + | (os = "macos" & arch = "arm64")) ] x-maintenance-intent: [ "(latest)" ] synopsis: "OCaml cross-compiler to the freestanding 64-bit ARM Solo5 backend" diff --git a/patches/5.4.1/0004-Makefile.cross-keep-function-sections-off-the-host-.patch b/patches/5.4.1/0004-Makefile.cross-keep-function-sections-off-the-host-.patch new file mode 100644 index 00000000..c8eb89ac --- /dev/null +++ b/patches/5.4.1/0004-Makefile.cross-keep-function-sections-off-the-host-.patch @@ -0,0 +1,42 @@ +From 3bf151678f524f65a96d0e2173735e37243b5082 Mon Sep 17 00:00:00 2001 +From: Thomas Gazagnaire +Date: Wed, 8 Jul 2026 23:43:23 -0700 +Subject: [PATCH 4/4] Makefile.cross: keep -function-sections off the host .opt + tools + +crossopt builds ocamlc.opt/ocamlopt.opt and the *.opt tools with the host +ocamlopt, but injects the target's OC_NATIVE_COMPFLAGS (which includes +-function-sections when the target supports it) into every native compile. +When the target enables function sections but the host does not -- an ELF +cross target built on a macOS host, whose ocamlopt is Mach-O and rejects +-function-sections -- crossopt fails building the host tools. + +Neutralise OC_NATIVE_COMPFLAGS for those host-only .opt builds; the target +library/otherlibs phases keep it, so target code is still emitted with +function sections (and can be garbage-collected by the linker). +--- + Makefile.cross | 8 ++++++-- + 1 file changed, 6 insertions(+), 2 deletions(-) + +diff --git a/Makefile.cross b/Makefile.cross +index b0a21c3d7c..af7e0d5b21 100644 +--- a/Makefile.cross ++++ b/Makefile.cross +@@ -76,11 +76,15 @@ endif + $(MAKE) dynlink-all $(CROSS_OVERRIDES) $(OLDS) + $(MAKE) -C otherlibs all $(CROSS_OVERRIDES) $(OLDS) + $(MAKE) runtimeopt $(OLDS) ++# Host binaries: drop OC_NATIVE_COMPFLAGS (e.g. -function-sections, which a ++# Mach-O host ocamlopt rejects); the target phases below keep it. + $(MAKE) ocamlc.opt ocamlopt.opt $(TOOLS_NATIVE_TARGETS) \ +- $(CROSS_COMPILER_OVERRIDES) "$(HOST_ZSTD_LIBS)" $(OLDS) ++ $(CROSS_COMPILER_OVERRIDES) OC_NATIVE_COMPFLAGS= \ ++ "$(HOST_ZSTD_LIBS)" $(OLDS) + $(MAKE) libraryopt $(CROSS_OVERRIDES) $(OLDS) + $(MAKE) otherlibrariesopt ocamltoolsopt $(CROSS_OVERRIDES) $(OLDS) +- $(MAKE) tools-allopt.opt $(CROSS_COMPILER_OVERRIDES) $(OLDS) ++ $(MAKE) tools-allopt.opt $(CROSS_COMPILER_OVERRIDES) OC_NATIVE_COMPFLAGS= \ ++ $(OLDS) + # We now build the compiler libs again, but for target this time + rm -f $(ocamlcommon_NCOBJS) $(ocamlmiddleend_NCOBJS) \ + $(ocamlbytecomp_NCOBJS) $(ocamloptcomp_NCOBJS) \ diff --git a/patches/5.5.0/0004-Makefile.cross-keep-function-sections-off-the-host-.patch b/patches/5.5.0/0004-Makefile.cross-keep-function-sections-off-the-host-.patch new file mode 100644 index 00000000..90dfc7e9 --- /dev/null +++ b/patches/5.5.0/0004-Makefile.cross-keep-function-sections-off-the-host-.patch @@ -0,0 +1,42 @@ +From e4dbf608b08d03e7af71e123ddddf9ad576e4092 Mon Sep 17 00:00:00 2001 +From: Thomas Gazagnaire +Date: Wed, 8 Jul 2026 23:51:53 -0700 +Subject: [PATCH 4/4] Makefile.cross: keep -function-sections off the host .opt + tools + +crossopt builds ocamlc.opt/ocamlopt.opt and the *.opt tools with the host +ocamlopt, but injects the target's OC_NATIVE_COMPFLAGS (which includes +-function-sections when the target supports it) into every native compile. +When the target enables function sections but the host does not -- an ELF +cross target built on a macOS host, whose ocamlopt is Mach-O and rejects +-function-sections -- crossopt fails building the host tools. + +Neutralise OC_NATIVE_COMPFLAGS for those host-only .opt builds; the target +library/otherlibs phases keep it, so target code is still emitted with +function sections (and can be garbage-collected by the linker). +--- + Makefile.cross | 8 ++++++-- + 1 file changed, 6 insertions(+), 2 deletions(-) + +diff --git a/Makefile.cross b/Makefile.cross +index cf069212ea..a33ad8f28d 100644 +--- a/Makefile.cross ++++ b/Makefile.cross +@@ -89,11 +89,15 @@ endif + $(MAKE) dynlink-all $(CROSS_OVERRIDES) $(OLDS) + $(MAKE) -C otherlibs all $(CROSS_OVERRIDES) $(OLDS) + $(MAKE) runtimeopt $(OLDS) ++# Host binaries: drop OC_NATIVE_COMPFLAGS (e.g. -function-sections, which a ++# Mach-O host ocamlopt rejects); the target phases below keep it. + $(MAKE) ocamlc.opt ocamlopt.opt $(TOOLS_NATIVE_TARGETS) \ +- $(CROSS_COMPILER_OVERRIDES) "$(HOST_ZSTD_LIBS)" $(OLDS) ++ $(CROSS_COMPILER_OVERRIDES) OC_NATIVE_COMPFLAGS= \ ++ "$(HOST_ZSTD_LIBS)" $(OLDS) + $(MAKE) libraryopt $(CROSS_OVERRIDES) $(OLDS) + $(MAKE) otherlibrariesopt ocamltoolsopt $(CROSS_OVERRIDES) $(OLDS) +- $(MAKE) tools-allopt.opt $(CROSS_COMPILER_OVERRIDES) $(OLDS) ++ $(MAKE) tools-allopt.opt $(CROSS_COMPILER_OVERRIDES) OC_NATIVE_COMPFLAGS= \ ++ $(OLDS) + # We now build the compiler libs again, but for target this time + rm -f $(ocamlcommon_NCOBJS) $(ocamlmiddleend_NCOBJS) \ + $(ocamlbytecomp_NCOBJS) $(ocamloptcomp_NCOBJS) \