From 68c3f3a49cb0ca48e7054962881167dc2eb8bcbc Mon Sep 17 00:00:00 2001 From: Wes John-Alder Date: Mon, 12 Oct 2026 08:23:54 -0400 Subject: [PATCH 1/2] Build the jar for four platforms Build the native library on Linux x86_64, Linux aarch64, macOS arm64, and macOS x86_64 runners, then package all four into one jar with no classifier. Run CI on pushes to master. Bump the version to 0.0.15 and raise the CMake floor so CMake 4 builds the library. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/ci.yml | 97 +++++++++++++++++-- .gitignore | 3 + Dockerfile | 17 ---- README.md | 41 +++++--- build.sbt | 11 +-- .../publish_local_with_lowered_log_levels.sh | 6 +- scripts/check-jar-platforms | 12 +++ scripts/silence-ecos-logs | 11 +++ src/native/CMakeLists.txt | 2 +- 9 files changed, 150 insertions(+), 50 deletions(-) delete mode 100644 Dockerfile create mode 100755 scripts/check-jar-platforms create mode 100755 scripts/silence-ecos-logs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5004e8..eae48c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,28 +3,105 @@ name: CI on: push: branches: - - main # Change to your default branch if different + - master pull_request: +permissions: + contents: read + env: - JAVA_OPTS: "-Xmx4G -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled" + SBT_OPTS: "-Xmx4G -XX:+UseG1GC" jobs: - build: + native: + name: Native library (${{ matrix.platform }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + # platform must equal the directory name sbt-jni derives from `uname -sm` + include: + - runner: ubuntu-latest + platform: x86_64-linux + - runner: ubuntu-24.04-arm + platform: aarch64-linux + - runner: macos-latest + platform: arm64-darwin + - runner: macos-15-intel + platform: x86_64-darwin + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + + - name: Set up sbt + uses: sbt/setup-sbt@v1 + + - name: Silence ECOS prints and profiling + run: ./scripts/silence-ecos-logs + + - name: Build the native library and run tests + run: sbt -Dsbt.log.noformat=true nativeCompile test + + - name: Check the platform directory name + run: ls "target/native/${{ matrix.platform }}/bin/" + + - name: Upload the native library + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.platform }} + path: target/native/${{ matrix.platform }}/bin/libecos0.* + if-no-files-found: error + + package: + name: Package every platform into one jar + needs: native runs-on: ubuntu-latest + permissions: + contents: read + packages: write steps: - name: Checkout code uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + + - name: Set up sbt + uses: sbt/setup-sbt@v1 + + - name: Silence ECOS prints and profiling + run: ./scripts/silence-ecos-logs + + # Each artifact is named by platform, so this lands them at lib_native//libecos0.*, + # the layout sbt-jni packages as native// in the jar. The host build of + # x86_64-linux maps to the same path and sbt-jni keeps one copy. + - name: Download the native libraries + uses: actions/download-artifact@v4 + with: + path: lib_native - - name: Build Docker image - run: docker build -t ecos-java-scala . + - name: Build the jar + run: sbt -Dsbt.log.noformat=true test package - - name: Run tests - run: docker run --rm ecos-java-scala:latest sbt -Dsbt.log.noformat=true +test + - name: Check the jar holds every platform + run: ./scripts/check-jar-platforms - - name: Publish to Github Repositories - if: github.event_name == 'push' && github.ref == 'refs/heads/${{ github.event.repository.default_branch }}' + - name: Publish to GitHub Packages + if: github.event_name == 'push' && github.ref == 'refs/heads/master' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_ACTOR: ${{ github.actor }} - run: docker run --rm -e GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} -e GITHUB_ACTOR=${{ github.actor }} -e PUBLISH_TO_GITHUB=true ecos-java-scala:latest sbt -Dsbt.log.noformat=true +publish \ No newline at end of file + run: sbt -Dsbt.log.noformat=true publish diff --git a/.gitignore b/.gitignore index 0f85256..b54f7ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # ignore target anywhere in tree target/ +# native libraries collected from other platforms before packaging +lib_native/ + # IntelliJ IDEA files .idea/ *.iml diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index d74e439..0000000 --- a/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -#syntax=docker/dockerfile:labs - - -FROM sbtscala/scala-sbt:eclipse-temurin-jammy-11.0.17_8_1.9.4_2.13.12 - -ENV SBT_OPTS="-Xmx4G -XX:+UseG1GC" - -WORKDIR /opt/ecos-java-scala - -RUN apt-get -yq update && apt-get -yq install cmake build-essential - -COPY . . - -ADD https://github.com/embotech/ecos.git ecos - -RUN sed -i 's/PRINTLEVEL (2)/PRINTLEVEL (0)/g' ecos/include/glblopts.h && \ - sed -i 's/PROFILING (1)/PROFILING (0)/g' ecos/include/glblopts.h \ No newline at end of file diff --git a/README.md b/README.md index 5b1fcaf..d1935b7 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,9 @@ amd and ldl JNI libraries are licensed under LGPL honoring original Tim Davis's ### Prerequisites * sbt - * cmake - * [optional] Java SDK 11 (for javah) + * cmake 3.10 or newer + * JDK 17 + * [optional] `javah` from a JDK 11 or older, to regenerate the JNI header ### Clone submodules @@ -47,12 +48,34 @@ To publish the Scala 2.13 version to local repository invoke: ./local_dev/publish_local_with_lowered_log_levels.sh ``` -The above will set the ecos log level configurtions to sane levels, compile native ecos library and put that into jar. Platform is added as classifier to the published jar. -For Mac OS below files will be generated: +The script lowers the ECOS log levels and builds the native library for this machine. +Then sbt packs that library into the jar and writes the jar to: ``` -${HOME}/.ivy2/local/io.citrine/ecos_2.13/X.X.X/jars/ecos_2.13-Mac_OS_X.jar +${HOME}/.ivy2/local/io.citrine/ecos_2.13/X.X.X/jars/ecos_2.13.jar ``` +A local jar holds one native library, the one for this machine, under `native/-/`. +On an Apple Silicon Mac that directory is `native/arm64-darwin/`. +The local ivy repository is first in sbt's resolver order, so mithril builds against the local jar at once. + +### Continuous integration and publishing + +The CI workflow builds the native library on four runners, one per platform: + +| Runner | Platform directory | +|--------|--------------------| +| `ubuntu-latest` | `native/x86_64-linux/` | +| `ubuntu-24.04-arm` | `native/aarch64-linux/` | +| `macos-latest` | `native/arm64-darwin/` | +| `macos-15-intel` | `native/x86_64-darwin/` | + +A final job collects the four libraries into one jar and checks that each directory is present. +On a push to `master` that job publishes the jar to GitHub Packages. +That jar has no classifier. +At runtime sbt-jni reads `uname -sm` and loads the library from the directory with that name. + +GitHub Packages rejects a second publish of the same version, so bump `ThisBuild / version` in `build.sbt` for every PR that changes the library. + ### Troubleshooting For certain combinations of Java, CMake, and XCode the `FindJNI` module does not work properly, leading to the following error during compilation: @@ -80,13 +103,9 @@ set(JAVA_AWT_INCLUDE_PATH "${JAVA_HOME}/include") ## Usage in Scala project -Add library dependency including classifier, e.g.: +Add the library dependency: ``` -private val osNameClassifier = System.getProperty("os.name").replace(' ', '_').trim -private val osArchitecture = System.getProperty("os.arch").replace(' ', '_').trim -private val artifactClassifier = osNameClassifier + "_" + osArchitecture -[...] -libraryDependencies += "io.citrine" %% "ecos" % "X.X.X" classifier artifactClassifier +libraryDependencies += "io.citrine" %% "ecos" % "X.X.X" ``` Native library will be loaded when instance of `NativeECOS` is created. diff --git a/build.sbt b/build.sbt index 54d85fc..49ea973 100644 --- a/build.sbt +++ b/build.sbt @@ -1,15 +1,10 @@ import Dependencies._ ThisBuild / scalaVersion := "2.13.15" -ThisBuild / version := "0.0.14" +ThisBuild / version := "0.0.15" ThisBuild / versionScheme := Some("early-semver") ThisBuild / organization := "io.citrine" ThisBuild / organizationName := "Citrine Informatics" -ThisBuild / artifactClassifier := Some(osNameClassifier + "_" + osArchitecture) - -// Publish versions based on OS name/cpu architecture -lazy val osNameClassifier = System.getProperty("os.name").replace(' ', '_').trim -lazy val osArchitecture = System.getProperty("os.arch").replace(' ', '_').trim ThisBuild / credentials += Credentials( "GitHub Package Registry", @@ -22,6 +17,10 @@ lazy val githubRepository = "GitHub Package Registry" at s"https://maven.pkg.git lazy val commonSettings = Seq( javah / target := sourceDirectory.value / "native" / "include", + // CI drops libraries from other runners into lib_native// before packaging. + // sbt-jni would also map lib_native/ under the host platform; turn that second mapping off. + Compile / unmanagedPlatformDependentNativeDirectories := Seq.empty, + Test / unmanagedPlatformDependentNativeDirectories := Seq.empty, crossPaths := true, packageDoc / publishArtifact := false, publishTo := Some(githubRepository) diff --git a/local_dev/publish_local_with_lowered_log_levels.sh b/local_dev/publish_local_with_lowered_log_levels.sh index 4105fc0..c4eff84 100755 --- a/local_dev/publish_local_with_lowered_log_levels.sh +++ b/local_dev/publish_local_with_lowered_log_levels.sh @@ -6,10 +6,6 @@ set -o pipefail # don't hide errors within pipes git submodule init git submodule update -## This is a hack to set the global logging levels down to a reasonable level -## Note that this differs from the commands in the Dockerfile due -## to the differences in GNU and BSD sed. See https://stackoverflow.com/questions/4247068/sed-command-with-i-option-failing-on-mac-but-works-on-linux -sed -i '' 's/PRINTLEVEL (2)/PRINTLEVEL (0)/g' ecos/include/glblopts.h -sed -i '' 's/PROFILING (1)/PROFILING (0)/g' ecos/include/glblopts.h +./scripts/silence-ecos-logs sbt publishLocal diff --git a/scripts/check-jar-platforms b/scripts/check-jar-platforms new file mode 100755 index 0000000..43f2d84 --- /dev/null +++ b/scripts/check-jar-platforms @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Fail unless the packaged jar holds a native library for every supported platform. +# Prerequisite: `sbt package` has run. +set -euo pipefail + +cd "$(dirname "$0")/.." + +jar=$(ls target/scala-2.13/ecos_2.13-*.jar) +unzip -l "$jar" | grep 'native/' +for platform in x86_64-linux aarch64-linux arm64-darwin x86_64-darwin; do + unzip -l "$jar" | grep -q "native/$platform/libecos0" || { echo "missing native/$platform/"; exit 1; } +done diff --git a/scripts/silence-ecos-logs b/scripts/silence-ecos-logs new file mode 100755 index 0000000..53a40fa --- /dev/null +++ b/scripts/silence-ecos-logs @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Turn off ECOS console prints and profiling before the native build. +# Prerequisite: the ecos submodule is checked out (git submodule update --init). +# Side effect: edits ecos/include/glblopts.h in the working tree. +set -euo pipefail + +cd "$(dirname "$0")/.." + +# -i.bak works with both GNU and BSD sed. +sed -i.bak 's/PRINTLEVEL (2)/PRINTLEVEL (0)/; s/PROFILING (1)/PROFILING (0)/' ecos/include/glblopts.h +rm ecos/include/glblopts.h.bak diff --git a/src/native/CMakeLists.txt b/src/native/CMakeLists.txt index 442bbbf..89d8718 100644 --- a/src/native/CMakeLists.txt +++ b/src/native/CMakeLists.txt @@ -3,7 +3,7 @@ # add/modify/remove settings to build your specific library. # ################################################################ -cmake_minimum_required(VERSION 3.0.0) +cmake_minimum_required(VERSION 3.10) # Define project and related variables # (required by sbt-jni) please use semantic versioning From d9d43eeb56f91b337fbfb4dc26177a2cf750b116 Mon Sep 17 00:00:00 2001 From: Wes John-Alder Date: Mon, 12 Oct 2026 08:32:34 -0400 Subject: [PATCH 2/2] Run CI on main, the new default branch Co-Authored-By: Claude Fable 5.1 --- .github/workflows/ci.yml | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eae48c5..9be7899 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: push: branches: - - master + - main pull_request: permissions: @@ -100,7 +100,7 @@ jobs: run: ./scripts/check-jar-platforms - name: Publish to GitHub Packages - if: github.event_name == 'push' && github.ref == 'refs/heads/master' + if: github.event_name == 'push' && github.ref == 'refs/heads/main' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_ACTOR: ${{ github.actor }} diff --git a/README.md b/README.md index d1935b7..4f16e99 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ The CI workflow builds the native library on four runners, one per platform: | `macos-15-intel` | `native/x86_64-darwin/` | A final job collects the four libraries into one jar and checks that each directory is present. -On a push to `master` that job publishes the jar to GitHub Packages. +On a push to `main` that job publishes the jar to GitHub Packages. That jar has no classifier. At runtime sbt-jni reads `uname -sm` and loads the library from the directory with that name.