From 18fd71570d1ca65cb8239d6668767216655f286b Mon Sep 17 00:00:00 2001 From: Olivier Chorchos Date: Thu, 23 Jul 2026 18:29:34 +0200 Subject: [PATCH 1/2] feat: add macOS native library support --- .github/workflows/ci.yml | 65 ++++++++++++++ .github/workflows/release.yml | 85 +++++++++++++++++-- CHANGELOG.md | 2 + README.md | 7 +- docs/USAGE.md | 29 ++++++- src/main/resources/native/README.md | 7 ++ .../macos-aarch64/pbivnorm.dylib.license | 4 + .../macos-x86_64/pbivnorm.dylib.license | 4 + 8 files changed, 190 insertions(+), 13 deletions(-) create mode 100644 src/main/resources/native/macos-aarch64/pbivnorm.dylib.license create mode 100644 src/main/resources/native/macos-x86_64/pbivnorm.dylib.license diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bee54a0..99ff228 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,3 +89,68 @@ jobs: if ($entries -notcontains "native/windows-x86_64/pbivnorm.dll") { throw "Bundled pbivnorm.dll is missing from $($mainJar.Name)." } + + macos-native-smoke: + name: macOS bundled dylib smoke test (${{ matrix.architecture }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - runner: macos-13 + architecture: x86_64 + - runner: macos-14 + architecture: aarch64 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 22 + cache: maven + + - name: Make Maven wrapper executable + run: chmod +x mvnw + + - name: Install Fortran compiler + run: | + if ! command -v gfortran >/dev/null 2>&1; then + brew install gcc + fi + compiler="$(command -v gfortran || true)" + if [ -z "$compiler" ]; then + compiler="$(find "$(brew --prefix gcc)/bin" -maxdepth 1 -type f -name 'gfortran-*' | sort | tail -n 1)" + fi + test -n "$compiler" + echo "GFORTRAN=$compiler" >> "$GITHUB_ENV" + + - name: Build native pbivnorm library + run: | + resourceDirectory="src/main/resources/native/macos-${{ matrix.architecture }}" + mkdir -p "$resourceDirectory" + "$GFORTRAN" -dynamiclib -O2 -std=legacy -ffixed-line-length-none \ + -static-libgfortran -static-libquadmath -static-libgcc \ + src/main/fortran/pbivnorm.f \ + -o "$resourceDirectory/libpbivnorm.dylib" + otool -L "$resourceDirectory/libpbivnorm.dylib" + if otool -L "$resourceDirectory/libpbivnorm.dylib" \ + | grep -E 'libgfortran|libquadmath|/opt/homebrew|/usr/local/opt'; then + echo "The bundled dylib must not depend on a Homebrew runtime." >&2 + exit 1 + fi + + - name: Build JAR and test bundled dylib + env: + THEGREEKLAB_PBIVNORM_PATH: "" + run: ./mvnw -B -ntp "-Dtest=BivariateNormalTest" "-Dsurefire.failIfNoSpecifiedTests=false" package + + - name: Verify dylib is packaged + run: | + mainJar="$(find thegreeklab-core/target -maxdepth 1 -name 'thegreeklab-*.jar' \ + ! -name '*-sources.jar' ! -name '*-javadoc.jar' | head -n 1)" + test -n "$mainJar" + jar tf "$mainJar" | grep -Fx "native/macos-${{ matrix.architecture }}/libpbivnorm.dylib" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5d60f28..14ae7e2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,9 +13,82 @@ concurrency: cancel-in-progress: false jobs: + native-libraries: + name: Build native library (${{ matrix.resource-directory }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + resource-directory: linux-x86_64 + library-name: libpbivnorm.so + platform: linux + - runner: macos-13 + resource-directory: macos-x86_64 + library-name: libpbivnorm.dylib + platform: macos + - runner: macos-14 + resource-directory: macos-aarch64 + library-name: libpbivnorm.dylib + platform: macos + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install macOS Fortran compiler + if: matrix.platform == 'macos' + run: | + if ! command -v gfortran >/dev/null 2>&1; then + brew install gcc + fi + compiler="$(command -v gfortran || true)" + if [ -z "$compiler" ]; then + compiler="$(find "$(brew --prefix gcc)/bin" -maxdepth 1 -type f -name 'gfortran-*' | sort | tail -n 1)" + fi + test -n "$compiler" + echo "GFORTRAN=$compiler" >> "$GITHUB_ENV" + + - name: Build native pbivnorm library + run: | + resourceDirectory="src/main/resources/native/${{ matrix.resource-directory }}" + mkdir -p "$resourceDirectory" + if [ "${{ matrix.platform }}" = "macos" ]; then + "$GFORTRAN" -dynamiclib -O2 -std=legacy -ffixed-line-length-none \ + -static-libgfortran -static-libquadmath -static-libgcc \ + src/main/fortran/pbivnorm.f \ + -o "$resourceDirectory/${{ matrix.library-name }}" + otool -L "$resourceDirectory/${{ matrix.library-name }}" + if otool -L "$resourceDirectory/${{ matrix.library-name }}" \ + | grep -E 'libgfortran|libquadmath|/opt/homebrew|/usr/local/opt'; then + echo "The bundled dylib must not depend on a Homebrew runtime." >&2 + exit 1 + fi + else + gfortran -shared -fPIC -O2 -std=legacy -ffixed-line-length-none \ + src/main/fortran/pbivnorm.f \ + -o "$resourceDirectory/${{ matrix.library-name }}" + fi + + - name: Upload native library + run: | + artifactDirectory="release-native/${{ matrix.resource-directory }}" + mkdir -p "$artifactDirectory" + cp "src/main/resources/native/${{ matrix.resource-directory }}/${{ matrix.library-name }}" \ + "$artifactDirectory/${{ matrix.library-name }}" + + - name: Upload native library + uses: actions/upload-artifact@v4 + with: + name: pbivnorm-${{ matrix.resource-directory }} + path: release-native + if-no-files-found: error + release: name: Build and publish release artifacts runs-on: ubuntu-latest + needs: native-libraries steps: - name: Checkout @@ -53,12 +126,12 @@ jobs: - name: Make Maven wrapper executable run: chmod +x mvnw - - name: Build native pbivnorm library - run: | - mkdir -p src/main/resources/native/linux-x86_64 - gfortran -shared -fPIC -O2 -std=legacy -ffixed-line-length-none \ - src/main/fortran/pbivnorm.f \ - -o src/main/resources/native/linux-x86_64/libpbivnorm.so + - name: Collect native pbivnorm libraries + uses: actions/download-artifact@v4 + with: + pattern: pbivnorm-* + path: src/main/resources/native + merge-multiple: true - name: Validate published POM version env: diff --git a/CHANGELOG.md b/CHANGELOG.md index b01f410..ebf96cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ The project follows [Semantic Versioning](https://semver.org/). with immutable bump scenarios, five standard Greeks and implied volatility. - Explicit `InvalidModelDomainException` reporting for numerically invalid model parameter regions. +- Bundled `pbivnorm` binaries and native smoke tests for macOS x86-64 and + Apple Silicon. ### Changed diff --git a/README.md b/README.md index 67bacaf..9e3ef36 100644 --- a/README.md +++ b/README.md @@ -80,9 +80,10 @@ numerical tests for vanilla European and American option models. The project includes Maven wrapper scripts, so a global Maven installation is not required. -A Windows x86-64 `pbivnorm.dll` is bundled with the project. GitHub Actions -builds the Linux x86-64 library from `src/main/fortran/pbivnorm.f` before -running Maven. Other platforms can supply an external library through: +Release artifacts bundle `pbivnorm` for Windows x86-64, Linux x86-64, macOS +x86-64 and macOS Apple Silicon. GitHub Actions builds the Linux and macOS +libraries from `src/main/fortran/pbivnorm.f` and smoke-tests each native +runtime. Other platforms can supply an external library through: ```text -Dthegreeklab.pbivnorm.path=/absolute/path/to/library diff --git a/docs/USAGE.md b/docs/USAGE.md index 0b7e57f..ed1e77b 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -704,13 +704,17 @@ import com.thegreeklab.math.BivariateNormal; double probability = BivariateNormal.cdf(0.25, -0.50, 0.60); ``` -Windows x86-64 uses the bundled library: +Release artifacts bundle the native library for Windows x86-64, Linux x86-64, +macOS x86-64 and macOS Apple Silicon. The runtime selects the matching resource +from `native/-/`. + +Windows x86-64 uses: ```text src/main/resources/native/windows-x86_64/pbivnorm.dll ``` -On Linux x86-64, build the library before running the application or tests: +When building from a source checkout on Linux x86-64, create: ```bash mkdir -p src/main/resources/native/linux-x86_64 @@ -719,6 +723,24 @@ gfortran -shared -fPIC -O2 -std=legacy -ffixed-line-length-none \ -o src/main/resources/native/linux-x86_64/libpbivnorm.so ``` +On macOS, the CI pipeline builds both Intel and Apple Silicon variants with +GNU Fortran. To build the matching library from a source checkout: + +```bash +brew install gcc +compiler="$(command -v gfortran || find "$(brew --prefix gcc)/bin" \ + -maxdepth 1 -type f -name 'gfortran-*' | sort | tail -n 1)" +case "$(uname -m)" in + x86_64) target=macos-x86_64 ;; + arm64) target=macos-aarch64 ;; + *) echo "Unsupported macOS architecture: $(uname -m)" >&2; exit 1 ;; +esac +mkdir -p "src/main/resources/native/$target" +"$compiler" -dynamiclib -O2 -std=legacy -ffixed-line-length-none \ + -static-libgfortran -static-libquadmath -static-libgcc src/main/fortran/pbivnorm.f \ + -o "src/main/resources/native/$target/libpbivnorm.dylib" +``` + An external library can be selected with a JVM property: ```bash @@ -734,8 +756,7 @@ THEGREEKLAB_PBIVNORM_PATH=/absolute/path/to/library ``` The external file must export either `pbivnorm_` (the usual GNU Fortran name) -or `pbivnorm`. macOS and ARM64 currently require an external build because no -matching binary is bundled. +or `pbivnorm`. ### Native component license diff --git a/src/main/resources/native/README.md b/src/main/resources/native/README.md index bf32777..b9b48e4 100644 --- a/src/main/resources/native/README.md +++ b/src/main/resources/native/README.md @@ -3,6 +3,13 @@ All `pbivnorm` native binaries in this directory are compiled from `src/main/fortran/pbivnorm.f` and are licensed under GPL-2.0-or-later. +Release builds package binaries in these runtime-selected directories: + +- `windows-x86_64` for Windows x86-64; +- `linux-x86_64` for Linux x86-64; +- `macos-x86_64` for Intel macOS; and +- `macos-aarch64` for Apple Silicon macOS. + The source is derived from the CRAN `pbivnorm` package version 0.6.0. Author attribution and provenance are recorded in the repository `NOTICE` file and in the comments retained in the Fortran source. diff --git a/src/main/resources/native/macos-aarch64/pbivnorm.dylib.license b/src/main/resources/native/macos-aarch64/pbivnorm.dylib.license new file mode 100644 index 0000000..4832248 --- /dev/null +++ b/src/main/resources/native/macos-aarch64/pbivnorm.dylib.license @@ -0,0 +1,4 @@ +SPDX-License-Identifier: GPL-2.0-or-later + +This binary is compiled from src/main/fortran/pbivnorm.f. See the repository +NOTICE file for provenance and author attribution. diff --git a/src/main/resources/native/macos-x86_64/pbivnorm.dylib.license b/src/main/resources/native/macos-x86_64/pbivnorm.dylib.license new file mode 100644 index 0000000..4832248 --- /dev/null +++ b/src/main/resources/native/macos-x86_64/pbivnorm.dylib.license @@ -0,0 +1,4 @@ +SPDX-License-Identifier: GPL-2.0-or-later + +This binary is compiled from src/main/fortran/pbivnorm.f. See the repository +NOTICE file for provenance and author attribution. From b83b911d585643e6fdd01b41c566dfe8479062b5 Mon Sep 17 00:00:00 2001 From: Olivier Chorchos Date: Thu, 23 Jul 2026 18:30:06 +0200 Subject: [PATCH 2/2] ci: verify native library packaging on Linux --- .github/workflows/ci.yml | 7 +++++++ README.md | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99ff228..7d5c56d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,13 @@ jobs: - name: Verify run: ./mvnw verify + - name: Verify Linux library is packaged + run: | + mainJar="$(find thegreeklab-core/target -maxdepth 1 -name 'thegreeklab-*.jar' \ + ! -name '*-sources.jar' ! -name '*-javadoc.jar' | head -n 1)" + test -n "$mainJar" + jar tf "$mainJar" | grep -Fx "native/linux-x86_64/libpbivnorm.so" + - name: Archive JaCoCo report uses: actions/upload-artifact@v4 with: diff --git a/README.md b/README.md index 9e3ef36..e4fddee 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,9 @@ not required. Release artifacts bundle `pbivnorm` for Windows x86-64, Linux x86-64, macOS x86-64 and macOS Apple Silicon. GitHub Actions builds the Linux and macOS libraries from `src/main/fortran/pbivnorm.f` and smoke-tests each native -runtime. Other platforms can supply an external library through: +runtime. CI validates native loading and JAR packaging on Windows x86-64, +Linux x86-64, macOS x86-64 and macOS Apple Silicon. Other platforms can supply +an external library through: ```text -Dthegreeklab.pbivnorm.path=/absolute/path/to/library