This repository contains an Android toolchain featuring OCaml 5.4.1, along
with cross-compiled opam packages to build against it.
The target is 64-bit ARM Android (aarch64-linux-android).
Whether a machine can build for it comes down to three things:
A native OCaml of the same version. Cross-compiling is not self-contained — the compiler builds its own tools with the host compiler, and dune, cppo and every ppx rewriter run natively throughout.
An NDK for that host. Google publishes prebuilt toolchains for a limited set of host architectures; see the NDK downloads for the current set. There is no supported way to target Android without one.
A POSIX shell. The packages here are shell scripts, and so are the NDK's own per-API compiler drivers, so the compiler cannot be invoked without one.
macOS and x86_64 Linux satisfy all three.
The Android NDK. This repository is developed and tested against r29. Older releases are untested.
macOS
brew install --cask android-ndk # -> /opt/homebrew/share/android-ndk
Debian / Ubuntu
apt search google-android-ndk # pick the newest r-NN installer
sudo apt install google-android-ndk-rNN-installer
The package downloads Google's NDK and unpacks it into Debian-friendly paths,
typically /usr/lib/android-ndk.
Fedora, or any distribution without a package
There is no NDK package in Fedora. Take it from Google directly — either the zip, unpacked wherever you like, or through the SDK manager:
sudo dnf install java-21-openjdk-headless
# unpack android-commandlinetools, then:
sdkmanager --list | grep ndk # then install a recent one
sdkmanager --install "ndk;<version>"
Add this repository to opam:
opam repository add android https://github.com/ocaml-cross/opam-cross-android.git
The version of the regular compiler installed in your current opam switch must match the version of the cross-compiler:
opam switch 5.4.1
eval $(opam env)
Install the compiler:
opam install ocaml-android
conf-ndk-android finds the NDK under the variables the Android tooling
already sets — ANDROID_NDK_ROOT, ANDROID_NDK_HOME, ANDROID_NDK_LATEST_HOME,
ANDROID_NDK — and on PATH. Point it at one if none of those is set:
ANDROID_NDK_ROOT=/opt/homebrew/share/android-ndk opam install ocaml-android
ANDROID_API selects the API level, the minimum Android version the resulting
binaries run on. It defaults to 26 and should match your app's minSdk:
ANDROID_API=21 opam install ocaml-android
Both are read when conf-ndk-android is built, so reinstall that package if you
switch NDKs or API levels. Neither is needed again when upgrading
ocaml-android.
dune build -x android _build/default.android/bin/your.exe
Naming the artifact rather than the target keeps dune to the cross context. A
bare dune build -x android builds the native context as well, which will want
the host libraries that a cross build has just been told to ignore.
The findlib toolchain is named android:
ocamlfind -toolchain android ocamlopt -config
Bionic has no pthread_cancel. OCaml's runtime calls it once, to cancel
straggler domains while the main one is terminating. ocaml-android patches
that call out and leaves the domain marked as cancelled, which is what stops the
teardown loop from freeing a lock the thread may still hold. The consequence is
narrow: a program that leaves extra domains running at caml_shutdown will not
force them to stop. pthread_cancel is best-effort even where it exists —
deferred cancellation only acts at cancellation points — so this is a difference
of degree. It matters most to embedders that call caml_shutdown and keep
running.
--disable-function-sections. configure enables it for an ELF target, but
the build machine compiles its own tools with the target's flags, and a macOS
ocamlopt reports function_sections: false — so a macOS host fails with
"OCaml has been configured without support for -function-sections". It is
disabled unconditionally rather than per-host, which costs a Linux host nothing
but --gc-sections dead-code trimming and keeps one build recipe.
Some OCaml packages bind a C library, which then has to exist for the target before they will build. This repository builds those libraries as well as the OCaml ones, so a cross switch can be assembled from opam alone.
build-android-sysroot declares one location, and everything for the target
lives under it: the cross compiler's own installation, the OCaml libraries built
against it, and the C libraries they bind. conf-<lib> packages look there and
build-<lib> packages install there.
The location is $(opam var prefix)/android-sysroot. dune installs cross-built
artifacts there. The findlib toolchain points there, and packages that do not
build with dune are told to install there.
build-<lib> — builds one library into the sysroot. Optional: install
it to have this repository build that library, or put your own cross-built
copy there and leave it out.
conf-<lib> — checks that the library is available for the target.
A package that depends on a conf-<lib>-android builds with
PKG_CONFIG_LIBDIR, LIBRARY_PATH and C_INCLUDE_PATH pointed at the sysroot
and PKG_CONFIG_PATH and CPATH cleared. The decision, for now, is to override
all of them: without it a build picks up system libraries and assumes they work
for the target.
This goes through build-env, on those packages only, so the rest of the switch
is untouched and native work in it behaves normally.
This may need revisiting as the repository grows — a cross-compiled package whose build needs a host binary linked against a host library would want both sets of paths at once.
Take the package's opam file from opam-repository and adapt it. The dependency
rewriting applies to any package; the build and install steps below are for
dune-based ones, which is most of what is here. A package with its own build
system needs its steps adapted to the cross compiler by hand — see
zarith-android for one that does.
depends: ocaml becomes ocaml-android, and each cross-compiled
dependency gains an -android suffix. Build-time tools — dune, cppo,
ppx runners, opam-installer — stay native. So do seq and bytes, which
the compiler provides and which are shipped here as findlib METAs only.depopts and conflicts need the same rewriting as depends. Missing this
is silent: an optional dependency that keeps its native name is simply never
detected.{with-test} and {with-doc} dependencies.url block unchanged.For a dune package:
build/install: add -x android, and use the package name rather than
name in -p.%{name}%.install after building.82 commits
This repository contains an Android toolchain featuring OCaml 5.4.1, along
with cross-compiled opam packages to build against it.
The target is 64-bit ARM Android (aarch64-linux-android).
Whether a machine can build for it comes down to three things:
A native OCaml of the same version. Cross-compiling is not self-contained — the compiler builds its own tools with the host compiler, and dune, cppo and every ppx rewriter run natively throughout.
An NDK for that host. Google publishes prebuilt toolchains for a limited set of host architectures; see the NDK downloads for the current set. There is no supported way to target Android without one.
A POSIX shell. The packages here are shell scripts, and so are the NDK's own per-API compiler drivers, so the compiler cannot be invoked without one.
macOS and x86_64 Linux satisfy all three.
The Android NDK. This repository is developed and tested against r29. Older releases are untested.
macOS
brew install --cask android-ndk # -> /opt/homebrew/share/android-ndk
Debian / Ubuntu
apt search google-android-ndk # pick the newest r-NN installer
sudo apt install google-android-ndk-rNN-installer
The package downloads Google's NDK and unpacks it into Debian-friendly paths,
typically /usr/lib/android-ndk.
Fedora, or any distribution without a package
There is no NDK package in Fedora. Take it from Google directly — either the zip, unpacked wherever you like, or through the SDK manager:
sudo dnf install java-21-openjdk-headless
# unpack android-commandlinetools, then:
sdkmanager --list | grep ndk # then install a recent one
sdkmanager --install "ndk;<version>"
Add this repository to opam:
opam repository add android https://github.com/ocaml-cross/opam-cross-android.git
The version of the regular compiler installed in your current opam switch must match the version of the cross-compiler:
opam switch 5.4.1
eval $(opam env)
Install the compiler:
opam install ocaml-android
conf-ndk-android finds the NDK under the variables the Android tooling
already sets — ANDROID_NDK_ROOT, ANDROID_NDK_HOME, ANDROID_NDK_LATEST_HOME,
ANDROID_NDK — and on PATH. Point it at one if none of those is set:
ANDROID_NDK_ROOT=/opt/homebrew/share/android-ndk opam install ocaml-android
ANDROID_API selects the API level, the minimum Android version the resulting
binaries run on. It defaults to 26 and should match your app's minSdk:
ANDROID_API=21 opam install ocaml-android
Both are read when conf-ndk-android is built, so reinstall that package if you
switch NDKs or API levels. Neither is needed again when upgrading
ocaml-android.
dune build -x android _build/default.android/bin/your.exe
Naming the artifact rather than the target keeps dune to the cross context. A
bare dune build -x android builds the native context as well, which will want
the host libraries that a cross build has just been told to ignore.
The findlib toolchain is named android:
ocamlfind -toolchain android ocamlopt -config
Bionic has no pthread_cancel. OCaml's runtime calls it once, to cancel
straggler domains while the main one is terminating. ocaml-android patches
that call out and leaves the domain marked as cancelled, which is what stops the
teardown loop from freeing a lock the thread may still hold. The consequence is
narrow: a program that leaves extra domains running at caml_shutdown will not
force them to stop. pthread_cancel is best-effort even where it exists —
deferred cancellation only acts at cancellation points — so this is a difference
of degree. It matters most to embedders that call caml_shutdown and keep
running.
--disable-function-sections. configure enables it for an ELF target, but
the build machine compiles its own tools with the target's flags, and a macOS
ocamlopt reports function_sections: false — so a macOS host fails with
"OCaml has been configured without support for -function-sections". It is
disabled unconditionally rather than per-host, which costs a Linux host nothing
but --gc-sections dead-code trimming and keeps one build recipe.
Some OCaml packages bind a C library, which then has to exist for the target before they will build. This repository builds those libraries as well as the OCaml ones, so a cross switch can be assembled from opam alone.
build-android-sysroot declares one location, and everything for the target
lives under it: the cross compiler's own installation, the OCaml libraries built
against it, and the C libraries they bind. conf-<lib> packages look there and
build-<lib> packages install there.
The location is $(opam var prefix)/android-sysroot. dune installs cross-built
artifacts there. The findlib toolchain points there, and packages that do not
build with dune are told to install there.
build-<lib> — builds one library into the sysroot. Optional: install
it to have this repository build that library, or put your own cross-built
copy there and leave it out.
conf-<lib> — checks that the library is available for the target.
A package that depends on a conf-<lib>-android builds with
PKG_CONFIG_LIBDIR, LIBRARY_PATH and C_INCLUDE_PATH pointed at the sysroot
and PKG_CONFIG_PATH and CPATH cleared. The decision, for now, is to override
all of them: without it a build picks up system libraries and assumes they work
for the target.
This goes through build-env, on those packages only, so the rest of the switch
is untouched and native work in it behaves normally.
This may need revisiting as the repository grows — a cross-compiled package whose build needs a host binary linked against a host library would want both sets of paths at once.
Take the package's opam file from opam-repository and adapt it. The dependency
rewriting applies to any package; the build and install steps below are for
dune-based ones, which is most of what is here. A package with its own build
system needs its steps adapted to the cross compiler by hand — see
zarith-android for one that does.
depends: ocaml becomes ocaml-android, and each cross-compiled
dependency gains an -android suffix. Build-time tools — dune, cppo,
ppx runners, opam-installer — stay native. So do seq and bytes, which
the compiler provides and which are shipped here as findlib METAs only.depopts and conflicts need the same rewriting as depends. Missing this
is silent: an optional dependency that keeps its native name is simply never
detected.{with-test} and {with-doc} dependencies.url block unchanged.For a dune package:
build/install: add -x android, and use the package name rather than
name in -p.%{name}%.install after building.82 commits