diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 553eaa2..5478f5a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -143,13 +143,6 @@ jobs: platform: x86_64-linux-musl - os: ubuntu-24.04-arm platform: aarch64-linux-musl - # OHOS (OpenHarmony / Huawei HarmonyOS PC) is musl-based arm64. - # Built inside the same Alpine container as aarch64-linux-musl; - # the resulting binary is byte-compatible. Only the gem's - # platform label differs so RubyGems on OHOS selects it. - - os: ubuntu-24.04-arm - platform: aarch64-linux-ohos - steps: - uses: actions/checkout@v4 @@ -174,27 +167,12 @@ jobs: git config --global --add safe.directory /work bundle install --jobs 4 bundle exec rake "gem:native:${PLATFORM}" - case "${PLATFORM}" in - *-ohos) - # OHOS gem is labeled aarch64-linux-ohos so RubyGems on OHOS - # selects it, but Alpine Ruby reports *-linux-musl -- a - # platform mismatch. The .so inside is musl-compatible, so - # unpack the gem and load it via -Ilib directly to verify - # the binary works without going through RubyGems install. - mkdir -p /tmp/ohos-smoke - gem unpack pkg/libpng-*.gem --target /tmp/ohos-smoke - (cd /tmp/ohos-smoke/libpng-* && \ - ruby -Ilib -e 'require "libpng"; png = Libpng.encode(2, 2, "\xff" * 16, pixel_format: "RGBA"); puts Libpng.decode(png, pixel_format: "RGBA").inspect') - ;; - *) - gem install -b pkg/libpng-*.gem - # Smoke test from /tmp so bundler's source-tree LOAD_PATH doesn't - # shadow the installed gem. Activate via `gem` so RubyGems resolves - # the platform-specific binary gem (Alpine Ruby's platform string - # is *-linux-musl, which must match the gem's platform suffix). - (cd /tmp && ruby -e 'gem "libpng"; require "libpng"; png = Libpng.encode(2, 2, "\xff" * 16, pixel_format: "RGBA"); File.binwrite("test.png", png); puts Libpng.decode(png, pixel_format: "RGBA").inspect') - ;; - esac + gem install -b pkg/libpng-*.gem + # Smoke test from /tmp so bundler's source-tree LOAD_PATH doesn't + # shadow the installed gem. Activate via `gem` so RubyGems resolves + # the platform-specific binary gem (Alpine Ruby's platform string + # is *-linux-musl, which must match the gem's platform suffix). + (cd /tmp && ruby -e 'gem "libpng"; require "libpng"; png = Libpng.encode(2, 2, "\xff" * 16, pixel_format: "RGBA"); File.binwrite("test.png", png); puts Libpng.decode(png, pixel_format: "RGBA").inspect') BUILD_EOF chmod +x /tmp/alpine-build.sh docker run --rm \ @@ -208,4 +186,51 @@ jobs: - uses: actions/upload-artifact@v4 with: name: pkg-${{ matrix.platform }} + path: pkg/*.gem + + build_ohos: + name: build aarch64-linux-ohos (OHOS NDK) + runs-on: ubuntu-latest + needs: prepare + steps: + - uses: actions/checkout@v4 + + - name: Install build tools + run: | + sudo apt-get update + sudo apt-get install -y cmake ninja-build zlib1g-dev qemu-user-static unzip jq curl + + - name: Setup Ruby + uses: ruby/setup-ruby@master + with: + ruby-version: ${{ needs.prepare.outputs.default-ruby-version }} + bundler-cache: true + + # Download OHOS SDK + LLVM-19 toolchain + sysroot via OpenHarmony's + # daily_build API. Output: OHOS_LLVM, OHOS_SYSROOT, OHOS_SIGN_TOOL, + # OHOS_NDK_ROOT env vars (written to $GITHUB_ENV by the script). + - name: Setup OHOS NDK + shell: bash + run: bash ext/ohos/setup-toolchain.sh + + # Cross-compile libpng via the OHOS NDK. recipe.rb detects + # OHOS_LLVM/OHOS_SYSROOT and uses ext/ohos/toolchain.cmake. + - name: Build gem + run: bundle exec rake gem:native:aarch64-linux-ohos + + # Smoke test: compile smoke-test.c with OHOS clang against the + # freshly-built libpng16.so, run under qemu-aarch64 with the OHOS + # sysroot as QEMU_LD_PREFIX. This verifies the .so loads and the + # simplified API round-trips correctly. + - name: qemu smoke test + shell: bash + run: bash ext/ohos/smoke-test.sh + + - name: Verify .so format + shell: bash + run: file lib/libpng/libpng16.so + + - uses: actions/upload-artifact@v4 + with: + name: pkg-aarch64-linux-ohos path: pkg/*.gem \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0a4cae4..9c73e03 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -201,10 +201,6 @@ jobs: platform: x86_64-linux-musl - os: ubuntu-24.04-arm platform: aarch64-linux-musl - # OHOS: same Alpine-built musl arm64 binary as aarch64-linux-musl, - # packaged under a distinct platform label. - - os: ubuntu-24.04-arm - platform: aarch64-linux-ohos steps: - uses: actions/checkout@v4 with: @@ -222,22 +218,8 @@ jobs: git config --global --add safe.directory /work bundle install --jobs 4 bundle exec rake "gem:native:${PLATFORM}" - case "${PLATFORM}" in - *-ohos) - # OHOS gem is labeled aarch64-linux-ohos so RubyGems on OHOS - # selects it, but Alpine Ruby reports *-linux-musl. The .so - # inside is musl-compatible -- unpack and load via -Ilib to - # verify the binary without going through RubyGems install. - mkdir -p /tmp/ohos-smoke - gem unpack pkg/libpng-*.gem --target /tmp/ohos-smoke - (cd /tmp/ohos-smoke/libpng-* && \ - ruby -Ilib -e 'require "libpng"; png = Libpng.encode(2, 2, "\xff" * 16, pixel_format: "RGBA"); puts Libpng.decode(png, pixel_format: "RGBA").inspect') - ;; - *) - gem install -b pkg/libpng-*.gem - (cd /tmp && ruby -e 'gem "libpng"; require "libpng"; png = Libpng.encode(2, 2, "\xff" * 16, pixel_format: "RGBA"); puts Libpng.decode(png, pixel_format: "RGBA").inspect') - ;; - esac + gem install -b pkg/libpng-*.gem + (cd /tmp && ruby -e 'gem "libpng"; require "libpng"; png = Libpng.encode(2, 2, "\xff" * 16, pixel_format: "RGBA"); puts Libpng.decode(png, pixel_format: "RGBA").inspect') BUILD_EOF chmod +x /tmp/alpine-build.sh docker run --rm \ @@ -253,8 +235,47 @@ jobs: name: pkg-${{ matrix.platform }} path: pkg/*.gem + build_ohos: + needs: bump + if: always() && !cancelled() && !failure() && (needs.bump.result == 'success' || needs.bump.result == 'skipped') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.bump.outputs.sha || github.ref }} + + - name: Install build tools + run: | + sudo apt-get update + sudo apt-get install -y cmake ninja-build zlib1g-dev qemu-user-static unzip jq curl + + - uses: ruby/setup-ruby@master + with: + ruby-version: '3.3' + bundler-cache: true + + - name: Setup OHOS NDK + shell: bash + run: bash ext/ohos/setup-toolchain.sh + + - name: Build gem + run: bundle exec rake gem:native:aarch64-linux-ohos + + - name: qemu smoke test + shell: bash + run: bash ext/ohos/smoke-test.sh + + - name: Verify .so format + shell: bash + run: file lib/libpng/libpng16.so + + - uses: actions/upload-artifact@v4 + with: + name: pkg-aarch64-linux-ohos + path: pkg/*.gem + publish: - needs: [ bump, build, build_musl ] + needs: [ bump, build, build_musl, build_ohos ] if: always() && !cancelled() && !failure() runs-on: ubuntu-latest steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 05b34fd..a02e4b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,54 @@ This gem follows a `{LIBPNG_VERSION}.{LIBPNG_RUBY_ITERATION}` version scheme. `LIBPNG_VERSION` is the upstream libpng release; `ITERATION` bumps for Ruby-side changes and resets to 0 when LIBPNG_VERSION bumps. +## [1.6.58.6] - 2026-07-26 + +### Changed +- **OHOS (`aarch64-linux-ohos`) is now cross-compiled with the proper + OpenHarmony NDK**, replacing the previous approach of shipping the + Alpine-built musl arm64 binary under the OHOS platform label. + The previous approach assumed byte-equivalence based solely on the + dynamic linker path matching (`/lib/ld-musl-aarch64.so.1`) -- an + unverified claim that ignored real ABI risks (musl patches, symbol + visibility, TLS layout, code signing). + + The new path uses the OpenHarmony daily_build API + (`dcp.openharmony.cn/api/daily_build/build/list/component`) to fetch + `ohos-sdk-public` + `LLVM-19`. The OHOS NDK's clang + (`aarch64-unknown-linux-ohos-clang`) builds libpng against the OHOS + sysroot; the resulting `.so` is signed with OHOS's + `binary-sign-tool -selfSign 1` (mandatory for runtime loading). + Verified via qemu-aarch64 smoke test (round-trip encode/decode + through libpng's simplified API). + + Reference: https://github.com/hqzing/ohos-node (build pattern). + +### Added +- `ext/ohos/setup-toolchain.sh` -- downloads + extracts OHOS SDK + + LLVM-19 + sysroot via the OpenHarmony daily_build API. +- `ext/ohos/toolchain.cmake` -- CMake cross-compile config. +- `ext/ohos/smoke-test.c` + `smoke-test.sh` -- minimal C round-trip + test, run via qemu-aarch64 with the OHOS sysroot as + `QEMU_LD_PREFIX`. +- New CI job `build_ohos` in `.github/workflows/build.yml` and + `release.yml`: runs on `ubuntu-latest`, sets up the OHOS NDK, + cross-compiles, signs the `.so`, runs the qemu smoke test. + +### Fixed +- `lib/libpng/recipe.rb` `setup_cross_compile` hook is now actually + populated for OHOS (was previously an empty "seam" comment). + +### Caveats +- The 1.6.58.4 and 1.6.58.5 `aarch64-linux-ohos` gems shipped with + Alpine-built bytes. They are superseded by 1.6.58.6's NDK build. + The Alpine-as-OHOS approach was an unverified assumption; whether + those binaries actually ran on OHOS hardware is unknown. If you + installed 1.6.58.4/.5 on OHOS and it worked, this version's + binary will differ but should be more correct. If it didn't work, + this version fixes it. +- OHOS NDK is a moving target (daily builds). The build IDs are + logged in CI output for traceability but not pinned. + ## [1.6.58.5] - 2026-07-26 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 6837fd4..0145ad6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,8 +29,11 @@ loaded via `autoload` from `lib/libpng.rb`. **Never use `require_relative` | `lib/libpng/standard_decoder.rb` | `Libpng::StandardDecoder` (libpng standard read API; explicit transform control) | | `lib/libpng/metadata_writer.rb` | `Libpng::MetadataWriter` (validates + writes text/gAMA/sRGB/cHRM/iCCP/pHYs onto a png_ptr/info_ptr pair) | | `lib/libpng/text_writer.rb` | `Libpng::TextWriter` + `Libpng::TextEntry` (builds png_text struct array, calls `png_set_text`) | -| `lib/libpng/recipe.rb` | `Libpng::Recipe < MiniPortileCMake` (builds libpng from source for the source gem) | +| `lib/libpng/recipe.rb` | `Libpng::Recipe < MiniPortileCMake` (builds libpng from source for the source gem; cross-compiles for OHOS via `ext/ohos/toolchain.cmake`) | | `ext/extconf.rb` | Gem extension entry. Triggers `Libpng::Recipe` autoload via `require 'libpng'`, then emits a dummy Makefile | +| `ext/ohos/setup-toolchain.sh` | Downloads + extracts OHOS SDK + LLVM-19 + sysroot via OpenHarmony daily_build API | +| `ext/ohos/toolchain.cmake` | CMake cross-compile config for OHOS NDK clang + sysroot | +| `ext/ohos/smoke-test.{c,sh}` | Minimal libpng round-trip test, run via qemu-aarch64 to verify the cross-compiled `.so` loads | ### Public API diff --git a/README.adoc b/README.adoc index 70ff588..5912028 100644 --- a/README.adoc +++ b/README.adoc @@ -44,7 +44,7 @@ for the current set): | `x86_64-linux-musl` | x86_64 Linux (musl, e.g. Alpine) | `ruby:-alpine` on x86_64 | `aarch64-linux` | ARM64 Linux (glibc) | `ubuntu-24.04-arm` (native) | `aarch64-linux-musl` | ARM64 Linux (musl) | `ruby:-alpine` on arm64 -| `aarch64-linux-ohos` | ARM64 OpenHarmony / Huawei HarmonyOS PC | `ruby:-alpine` on arm64 (byte-compatible with `aarch64-linux-musl`) +| `aarch64-linux-ohos` | ARM64 OpenHarmony / Huawei HarmonyOS PC | `ubuntu-latest` cross-compile via OHOS NDK (signed with `binary-sign-tool`) | `x64-mingw32` | x64 Windows, RubyInstaller < 3.0 (MSVCRT) | `windows-latest` | `x64-mingw-ucrt` | x64 Windows, RubyInstaller >= 3.0 (UCRT) | `windows-latest` | `aarch64-mingw-ucrt` | ARM64 Windows (Ruby >= 3.4) | `windows-11-arm` (native) diff --git a/ext/ohos/setup-toolchain.sh b/ext/ohos/setup-toolchain.sh new file mode 100755 index 0000000..e479913 --- /dev/null +++ b/ext/ohos/setup-toolchain.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# Downloads and extracts the OpenHarmony NDK on a Linux x86_64 host. +# Outputs four environment variables for subsequent build steps: +# +# OHOS_LLVM -- the LLVM toolchain dir (contains bin/, lib/, ...) +# OHOS_SYSROOT -- the OHOS sysroot (musl + system headers) +# OHOS_SIGN_TOOL -- binary-sign-tool used to sign the final .so +# OHOS_NDK_ROOT -- parent directory of everything (for reference) +# +# Both SDK and LLVM come from OpenHarmony's daily_build API at +# dcp.openharmony.cn. There's no version pinning here -- the API +# returns the latest successful build, which changes over time. The +# build ID is logged so reproducing a specific build is possible if +# needed. +# +# Reference: https://github.com/hqzing/ohos-node/blob/main/build.sh +set -euo pipefail + +OHOS_NDK_ROOT="${OHOS_NDK_ROOT:-/tmp/ohos-ndk}" +mkdir -p "$OHOS_NDK_ROOT" +cd "$OHOS_NDK_ROOT" + +# apt deps: curl, jq, tar, unzip. The workflow installs these; this +# script assumes they're available. +query_component() { + local component=$1 + curl -fsSL 'https://dcp.openharmony.cn/api/daily_build/build/list/component' \ + -H 'Accept: application/json, text/plain, */*' \ + -H 'Content-Type: application/json' \ + --data-raw '{"projectName":"openharmony","branch":"master","pageNum":1,"pageSize":10,"deviceLevel":"","component":"'"${component}"'","type":1,"startTime":"2025080100000000","endTime":"20990101235959","sortType":"","sortField":"","hardwareBoard":"","buildStatus":"success","buildFailReason":"","withDomain":1}' +} + +# 1. OHOS SDK (provides binary-sign-tool) +SDK_URL=$(query_component "ohos-sdk-public" | jq -r '.data.list.dataList[0].obsPath') +SDK_BUILD_ID=$(query_component "ohos-sdk-public" | jq -r '.data.list.dataList[0].buildId') +echo "OHOS SDK build: $SDK_BUILD_ID ($SDK_URL)" +curl -fsSL "$SDK_URL" -o ohos-sdk-public.tar.gz +tar -zxf ohos-sdk-public.tar.gz +# Inside ohos-sdk/linux/ there's toolchains-*.zip +unzip -q -o ohos-sdk/linux/toolchains-*.zip -d ohos-sdk/linux/ + +# 2. LLVM-19 (compilers + sysroot) +LLVM_URL=$(query_component "LLVM-19" | jq -r '.data.list.dataList[0].obsPath') +LLVM_BUILD_ID=$(query_component "LLVM-19" | jq -r '.data.list.dataList[0].buildId') +echo "LLVM-19 build: $LLVM_BUILD_ID ($LLVM_URL)" +curl -fsSL "$LLVM_URL" -o LLVM-19.tar.gz +mkdir -p llvm-19 +tar -zxf LLVM-19.tar.gz -C llvm-19 +( + cd llvm-19 + tar -zxf llvm-linux-x86_64.tar.gz + tar -zxf ohos-sysroot.tar.gz +) + +# The ohos-sysroot.tar.gz extracts to a layout like: +# llvm-19/sysroot/aarch64-linux-ohos/{Scrt1.o,libc.so,...} +# llvm-19/sysroot/usr/include/... +# Find Scrt1.o and back-compute the sysroot + multiarch paths from its +# location. This handles both layouts (sysroot/aarch64-linux-ohos and +# sysroot/usr/lib/aarch64-linux-ohos) without hard-coding depth. +CRT_FILE=$(find "$OHOS_NDK_ROOT/llvm-19" -name 'Scrt1.o' -path '*aarch64-linux-ohos*' 2>/dev/null | head -1) +if [ -z "$CRT_FILE" ]; then + echo "ERROR: could not find Scrt1.o under $OHOS_NDK_ROOT/llvm-19" >&2 + echo "Layout found:" >&2 + find "$OHOS_NDK_ROOT/llvm-19" -maxdepth 4 -type d >&2 + exit 1 +fi +# LIB_DIR = directory containing Scrt1.o (= the multiarch lib dir). +LIB_DIR=$(cd "$(dirname "$CRT_FILE")" && pwd) +# SYSROOT = nearest ancestor named "sysroot", else parent of the +# multiarch dir. The OHOS layout uses sysroot/aarch64-linux-ohos; +# a more conventional layout would have sysroot/usr/lib/aarch64-linux-ohos. +SYSROOT=$(cd "$LIB_DIR" && pwd) +while [ "$(basename "$SYSROOT")" != "sysroot" ] && [ "$SYSROOT" != "/" ]; do + SYSROOT=$(dirname "$SYSROOT") +done +if [ "$SYSROOT" = "/" ]; then + # No 'sysroot' dir found; fall back to LIB_DIR's parent. + SYSROOT=$(dirname "$LIB_DIR") +fi +echo "Detected OHOS sysroot: $SYSROOT" +echo "Detected OHOS lib dir: $LIB_DIR" +echo "Sample files in lib dir:" +ls -la "$LIB_DIR" | head -10 || true + +# libpng depends on zlib. Find zlib in the OHOS sysroot so we can +# tell CMake exactly where it lives (FindZLIB with cross-compile +# restrictions can't auto-discover it in OHOS's non-standard layout). +ZLIB_LIB=$(find "$SYSROOT" -name 'libz.so' -o -name 'libz.so.*' 2>/dev/null | grep -v '\.debug' | head -1) +ZLIB_INCLUDE=$(find "$SYSROOT" -name 'zlib.h' 2>/dev/null | head -1) +if [ -z "$ZLIB_LIB" ] || [ -z "$ZLIB_INCLUDE" ]; then + echo "ERROR: zlib not found under $SYSROOT" >&2 + echo " libz.so: ${ZLIB_LIB:-MISSING}" >&2 + echo " zlib.h: ${ZLIB_INCLUDE:-MISSING}" >&2 + exit 1 +fi +ZLIB_LIB_DIR=$(cd "$(dirname "$ZLIB_LIB")" && pwd) +ZLIB_INCLUDE_DIR=$(cd "$(dirname "$ZLIB_INCLUDE")" && pwd) +echo "Detected zlib lib: $ZLIB_LIB" +echo "Detected zlib include: $ZLIB_INCLUDE_DIR" + +# 3. Export env vars (for GitHub Actions; harmless elsewhere) +if [ -n "${GITHUB_ENV:-}" ]; then + echo "OHOS_NDK_ROOT=$OHOS_NDK_ROOT" >> "$GITHUB_ENV" + echo "OHOS_LLVM=$OHOS_NDK_ROOT/llvm-19/llvm" >> "$GITHUB_ENV" + echo "OHOS_SYSROOT=$SYSROOT" >> "$GITHUB_ENV" + echo "OHOS_LIB_DIR=$LIB_DIR" >> "$GITHUB_ENV" + echo "OHOS_ZLIB_LIBRARY=$ZLIB_LIB" >> "$GITHUB_ENV" + echo "OHOS_ZLIB_INCLUDE_DIR=$ZLIB_INCLUDE_DIR" >> "$GITHUB_ENV" + echo "OHOS_SIGN_TOOL=$OHOS_NDK_ROOT/ohos-sdk/linux/toolchains/lib/binary-sign-tool" >> "$GITHUB_ENV" + # Build IDs for traceability + echo "OHOS_SDK_BUILD_ID=$SDK_BUILD_ID" >> "$GITHUB_ENV" + echo "OHOS_LLVMBUILD_ID=$LLVM_BUILD_ID" >> "$GITHUB_ENV" +fi + +echo "OHOS NDK setup complete:" +echo " OHOS_LLVM=$OHOS_NDK_ROOT/llvm-19/llvm" +echo " OHOS_SYSROOT=$SYSROOT" +echo " OHOS_LIB_DIR=$LIB_DIR" +echo " OHOS_ZLIB_LIBRARY=$ZLIB_LIB" +echo " OHOS_ZLIB_INCLUDE_DIR=$ZLIB_INCLUDE_DIR" +echo " OHOS_SIGN_TOOL=$OHOS_NDK_ROOT/ohos-sdk/linux/toolchains/lib/binary-sign-tool" diff --git a/ext/ohos/smoke-test.c b/ext/ohos/smoke-test.c new file mode 100644 index 0000000..0e007ad --- /dev/null +++ b/ext/ohos/smoke-test.c @@ -0,0 +1,76 @@ +/* Minimal libpng round-trip test for cross-compiled OHOS binaries. + * Built with the OHOS NDK clang and run via qemu-aarch64 -L $SYSROOT + * to verify the .so loads and the simplified API works correctly. + * + * We can't run Ruby here (no Ruby port for OHOS), but we can verify + * that libpng's API surface behaves -- which is what the gem's FFI + * bindings exercise. + */ +#include +#include +#include +#include + +int main(void) { + png_image img = {0}; + img.version = PNG_IMAGE_VERSION; + img.width = 2; + img.height = 2; + img.format = PNG_FORMAT_RGBA; + + unsigned char pixels[16]; + int i; + for (i = 0; i < 16; i++) { + pixels[i] = (unsigned char)i; + } + + /* Encode */ + png_alloc_size_t size = 0; + if (!png_image_write_to_memory(&img, NULL, &size, 0, pixels, 0, NULL)) { + fprintf(stderr, "write_to_memory (size query) failed: %s\n", img.message); + return 1; + } + void *buf = malloc(size); + if (buf == NULL) { + fprintf(stderr, "malloc(%zu) failed\n", size); + return 1; + } + if (!png_image_write_to_memory(&img, buf, &size, 0, pixels, 0, NULL)) { + fprintf(stderr, "write_to_memory failed: %s\n", img.message); + free(buf); + return 1; + } + png_image_free(&img); + + /* Decode */ + png_image read_img = {0}; + read_img.version = PNG_IMAGE_VERSION; + if (!png_image_begin_read_from_memory(&read_img, buf, size)) { + fprintf(stderr, "begin_read_from_memory failed: %s\n", read_img.message); + free(buf); + return 1; + } + read_img.format = PNG_FORMAT_RGBA; + + unsigned char out_pixels[16] = {0}; + if (!png_image_finish_read(&read_img, NULL, out_pixels, 0, NULL)) { + fprintf(stderr, "finish_read failed: %s\n", read_img.message); + free(buf); + return 1; + } + png_image_free(&read_img); + free(buf); + + /* Verify */ + if (memcmp(pixels, out_pixels, 16) != 0) { + fprintf(stderr, "pixel mismatch:\n in : "); + for (i = 0; i < 16; i++) fprintf(stderr, "%02x ", pixels[i]); + fprintf(stderr, "\n out: "); + for (i = 0; i < 16; i++) fprintf(stderr, "%02x ", out_pixels[i]); + fprintf(stderr, "\n"); + return 1; + } + + printf("OK\n"); + return 0; +} diff --git a/ext/ohos/smoke-test.sh b/ext/ohos/smoke-test.sh new file mode 100755 index 0000000..34ef8bc --- /dev/null +++ b/ext/ohos/smoke-test.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Compiles ext/ohos/smoke-test.c against the freshly-built libpng16.so +# using the OHOS NDK clang, then runs it under qemu-aarch64 with the +# OHOS sysroot as QEMU_LD_PREFIX so the dynamic linker resolves +# against OHOS's musl libc rather than the host's. +# +# Inputs (env vars, set by setup-toolchain.sh): +# OHOS_LLVM -- OHOS NDK LLVM dir (clang lives in bin/) +# OHOS_SYSROOT -- OHOS sysroot (musl + headers) +# +# The .so under test is at lib/libpng/libpng16.so (relative to repo +# root) after `rake compile` finishes. +set -euo pipefail + +REPO_ROOT="${REPO_ROOT:-$(pwd)}" +SO_PATH="$REPO_ROOT/lib/libpng/libpng16.so" + +if [ ! -f "$SO_PATH" ]; then + echo "smoke-test: $SO_PATH not found; did rake compile run?" >&2 + exit 1 +fi + +# Use the libpng headers that ship with the source tree (under +# ports/.../include after MiniPortile cooks), or fall back to the +# host's libpng-dev if not available. +INCLUDES=(-I"$REPO_ROOT/lib/libpng") +if [ -d "$REPO_ROOT/ports/ports/libpng-$(ruby -I"$REPO_ROOT/lib" -rlibpng/version -e 'puts Libpng::LIBPNG_VERSION')/include" ]; then + INCLUDES+=(-I"$REPO_ROOT/ports/ports/libpng-$(ruby -I"$REPO_ROOT/lib" -rlibpng/version -e 'puts Libpng::LIBPNG_VERSION')/include") +elif pkg-config --exists libpng; then + INCLUDES+=($(pkg-config --cflags libpng)) +fi + +CC="${OHOS_LLVM}/bin/aarch64-unknown-linux-ohos-clang" +SYSROOT="${OHOS_SYSROOT}" + +# Compile + link. rpath=$ORIGIN so the binary finds the .so next to +# it at runtime. We also pass --sysroot for the libc headers. +"$CC" \ + --sysroot="$SYSROOT" \ + "${INCLUDES[@]}" \ + -L"$REPO_ROOT/lib/libpng" \ + -Wl,-rpath=\$ORIGIN \ + -o /tmp/ohos-smoke-test \ + "$REPO_ROOT/ext/ohos/smoke-test.c" \ + -lpng16 -lz + +# Copy the .so next to the binary so rpath=$ORIGIN finds it. +cp "$SO_PATH" /tmp/libpng16.so + +# Run via qemu. QEMU_LD_PREFIX tells qemu where to find the dynamic +# linker (/lib/ld-musl-aarch64.so.1 inside the OHOS sysroot). +echo "Running smoke test under qemu-aarch64..." +QEMU_LD_PREFIX="$SYSROOT" qemu-aarch64 /tmp/ohos-smoke-test diff --git a/ext/ohos/toolchain.cmake b/ext/ohos/toolchain.cmake new file mode 100644 index 0000000..eec1432 --- /dev/null +++ b/ext/ohos/toolchain.cmake @@ -0,0 +1,40 @@ +# CMake toolchain for cross-compiling libpng to OHOS (OpenHarmony / +# Huawei HarmonyOS PC). Used by lib/libpng/recipe.rb when the OHOS +# NDK is available (OHOS_LLVM + OHOS_SYSROOT + OHOS_LIB_DIR env vars +# set by ext/ohos/setup-toolchain.sh). +# +# CMAKE_SYSTEM_NAME is set to "Linux" rather than "OHOS" because +# libpng's CMakeLists doesn't know about OHOS and OHOS's ABI is +# musl-linux-compatible at the file-format level. The sysroot provides +# the actual libc / headers; the system name just tells CMake's +# find_library / try_compile machinery to use the cross toolchain. +# +# The OHOS clang doesn't auto-detect the multiarch library path +# (.../usr/lib/aarch64-linux-ohos) when --sysroot is set, so we add +# it explicitly via CMAKE_EXE_LINKER_FLAGS / CMAKE_SHARED_LINKER_FLAGS. +# Without this, ld.lld fails with "cannot open Scrt1.o / crti.o / -lc". + +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_PROCESSOR aarch64) + +set(OHOS_LLVM "$ENV{OHOS_LLVM}") +set(OHOS_SYSROOT "$ENV{OHOS_SYSROOT}") +set(OHOS_LIB_DIR "$ENV{OHOS_LIB_DIR}") + +set(CMAKE_C_COMPILER "${OHOS_LLVM}/bin/aarch64-unknown-linux-ohos-clang") +set(CMAKE_CXX_COMPILER "${OHOS_LLVM}/bin/aarch64-unknown-linux-ohos-clang++") +set(CMAKE_AR "${OHOS_LLVM}/bin/llvm-ar" CACHE FILEPATH "Archiver") +set(CMAKE_RANLIB "${OHOS_LLVM}/bin/llvm-ranlib" CACHE FILEPATH "Ranlib") + +set(CMAKE_SYSROOT "${OHOS_SYSROOT}") +set(CMAKE_FIND_ROOT_PATH "${OHOS_SYSROOT}") +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + +# Explicit linker search paths so ld.lld finds Scrt1.o, crti.o, +# crtn.o, libc.so, etc. inside the multiarch subdirectory. +set(CMAKE_EXE_LINKER_FLAGS "--sysroot=${OHOS_SYSROOT} -L${OHOS_LIB_DIR}") +set(CMAKE_SHARED_LINKER_FLAGS "--sysroot=${OHOS_SYSROOT} -L${OHOS_LIB_DIR}") +set(CMAKE_MODULE_LINKER_FLAGS "--sysroot=${OHOS_SYSROOT} -L${OHOS_LIB_DIR}") diff --git a/lib/libpng/recipe.rb b/lib/libpng/recipe.rb index 70b8439..9f06571 100644 --- a/lib/libpng/recipe.rb +++ b/lib/libpng/recipe.rb @@ -64,6 +64,18 @@ def configure_defaults # macOS: avoid the .framework build; we want a plain .dylib. opts << '-DCMAKE_INSTALL_LIBDIR=lib' opts << '-DCMAKE_BUILD_TYPE=Release' + # OHOS NDK cross-compile: point CMake at the OHOS toolchain file + # set up by setup_cross_compile. The toolchain file points at + # the OHOS clang + sysroot. Also explicitly pass zlib (libpng's + # only required dependency) since CMake's FindZLIB can't auto- + # discover it in OHOS's non-standard sysroot layout. + if target_platform == 'aarch64-linux-ohos' && ENV['OHOS_TOOLCHAIN_FILE'] + opts << "-DCMAKE_TOOLCHAIN_FILE=#{ENV.fetch('OHOS_TOOLCHAIN_FILE', nil)}" + if ENV.fetch('OHOS_ZLIB_LIBRARY', nil) + opts << "-DZLIB_LIBRARY=#{ENV.fetch('OHOS_ZLIB_LIBRARY', nil)}" + opts << "-DZLIB_INCLUDE_DIR=#{ENV.fetch('OHOS_ZLIB_INCLUDE_DIR', nil)}" + end + end opts end @@ -80,9 +92,31 @@ def install FileUtils.mkdir_p(target_dir) FileUtils.cp_r(libs, target_dir, verbose: true) + # OHOS requires all executables and shared libraries to be code- + # signed before they can be loaded at runtime. The OHOS SDK ships + # binary-sign-tool; setup-toolchain.sh exposes its path via the + # OHOS_SIGN_TOOL env var. Without this, the .so won't load on a + # real OHOS device. + sign_ohos_libs if sign_ohos? + verify_libs end + def sign_ohos? + target_platform == 'aarch64-linux-ohos' && ENV.fetch('OHOS_SIGN_TOOL', nil) + end + + def sign_ohos_libs + sign_tool = ENV.fetch('OHOS_SIGN_TOOL', nil) + each_built_lib do |path| + message("Signing OHOS lib #{path}...\n") + system(sign_tool, 'sign', + '-inFile', path, '-outFile', path, + '-selfSign', '1', + exception: true) + end + end + def verify_libs each_built_lib do |path| out, st = Open3.capture2("file #{path}") @@ -175,11 +209,10 @@ def target_platform when /\A(arm64|aarch64).*linux-musl/ 'aarch64-linux-musl' when /\A(arm64|aarch64).*linux-ohos/ - # OHOS (OpenHarmony / Huawei HarmonyOS PC) is musl-based arm64. - # The resulting binary is ELF64 aarch64 linked against musl, - # identical at the file-format level to aarch64-linux-musl -- - # only the gem's platform label differs so RubyGems on OHOS - # selects the right variant. + # OHOS (OpenHarmony / Huawei HarmonyOS PC) target. The build + # itself is cross-compiled from x86_64 Linux using the OHOS + # NDK when OHOS_LLVM/OHOS_SYSROOT env vars are set (see + # ext/ohos/setup-toolchain.sh); see setup_cross_compile. 'aarch64-linux-ohos' when /\A(arm64|aarch64).*linux/ 'aarch64-linux' @@ -196,11 +229,19 @@ def cross_compile? # Configure MiniPortile + CMake for cross-compilation. Native builds # (host_platform == target_platform) skip this entirely. + # + # OHOS is the only target that actually cross-compiles today: the + # build host is x86_64 Ubuntu, the target is aarch64 OHOS. The + # OpenHarmony NDK (downloaded by ext/ohos/setup-toolchain.sh) + # provides the clang compiler + sysroot + binary-sign-tool. def setup_cross_compile - # All targeted platforms now have native runners (ubuntu-24.04-arm for - # aarch64-linux, windows-11-arm for aarch64-mingw-ucrt, Alpine containers - # for the musl variants). This hook is kept as a seam for future - # cross-compile targets (e.g. aarch64-linux on an x86_64 host). + return unless target_platform == 'aarch64-linux-ohos' + return unless ENV['OHOS_LLVM'] && ENV['OHOS_SYSROOT'] + + @host = 'aarch64-unknown-linux-ohos' + # Make CMake pick up our toolchain file when MiniPortile invokes + # it. configure_defaults appends the -DCMAKE_TOOLCHAIN_FILE flag. + ENV['OHOS_TOOLCHAIN_FILE'] = File.expand_path('ext/ohos/toolchain.cmake', ROOT) end def cpu_type diff --git a/lib/libpng/version.rb b/lib/libpng/version.rb index 8f5ac20..a0f61c0 100644 --- a/lib/libpng/version.rb +++ b/lib/libpng/version.rb @@ -11,6 +11,6 @@ module Libpng # new libpng release. The iteration resets to 0 each time # LIBPNG_VERSION bumps. LIBPNG_VERSION = '1.6.58' - LIBPNG_RUBY_ITERATION = 5 + LIBPNG_RUBY_ITERATION = 6 VERSION = "#{LIBPNG_VERSION}.#{LIBPNG_RUBY_ITERATION}" end