Skip to content
Closed
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
81 changes: 53 additions & 28 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 \
Expand All @@ -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
63 changes: 42 additions & 21 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 \
Expand All @@ -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:
Expand Down
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ for the current set):
| `x86_64-linux-musl` | x86_64 Linux (musl, e.g. Alpine) | `ruby:<ver>-alpine` on x86_64
| `aarch64-linux` | ARM64 Linux (glibc) | `ubuntu-24.04-arm` (native)
| `aarch64-linux-musl` | ARM64 Linux (musl) | `ruby:<ver>-alpine` on arm64
| `aarch64-linux-ohos` | ARM64 OpenHarmony / Huawei HarmonyOS PC | `ruby:<ver>-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)
Expand Down
122 changes: 122 additions & 0 deletions ext/ohos/setup-toolchain.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading
Loading