Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 $< $* > $@
Expand All @@ -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 $< $* > $@
Expand Down
23 changes: 23 additions & 0 deletions configure.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
;;
Expand All @@ -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"
Expand All @@ -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
20 changes: 14 additions & 6 deletions gen_toolchain_tool.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@
# 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

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

Expand Down Expand Up @@ -60,7 +62,6 @@ if [ -z "\$compiling" ]; then
-Wl,--start-group \\
-lnolibc \\
-lopenlibm \\
$EXTRALIBS \\
-Wl,--end-group
fi

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions nolibc/vfprintf.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down Expand Up @@ -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);
}
}

Expand Down
7 changes: 5 additions & 2 deletions ocaml-solo5-cross-aarch64.opam
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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: [
Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
From 3bf151678f524f65a96d0e2173735e37243b5082 Mon Sep 17 00:00:00 2001
From: Thomas Gazagnaire <thomas@gazagnaire.org>
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) \
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
From e4dbf608b08d03e7af71e123ddddf9ad576e4092 Mon Sep 17 00:00:00 2001
From: Thomas Gazagnaire <thomas@gazagnaire.org>
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) \
Loading