Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6c636a3
Add TODO.ohos/ design docs for OHOS NDK pivot
ronaldtse Jul 26, 2026
44c3150
Add OHOS NDK cross-compile support (ext/ohos + lib/libpng/ohos)
ronaldtse Jul 26, 2026
54b265c
Update CI workflows: dedicated build_ohos job for NDK cross-compile
ronaldtse Jul 26, 2026
6eb3a59
Docs + version bump for OHOS NDK support (1.6.58.6)
ronaldtse Jul 26, 2026
c0263fc
Fix setup-ndk.sh: SDK tarball already contains ohos-sdk/ at top level
ronaldtse Jul 26, 2026
320991f
Add diagnostics to setup-ndk.sh to find actual NDK paths
ronaldtse Jul 26, 2026
1765b95
Fix NDK paths + extract all SDK zips + richer diagnostics
ronaldtse Jul 26, 2026
b9c2a88
Register binfmt with fix-binary flag for x86_64 NDK binaries
ronaldtse Jul 26, 2026
93b5588
Drop broken binfmt smoke check (Alpine gcc lacks stdio.h)
ronaldtse Jul 26, 2026
2de6dbf
Provide x86_64 loader for qemu-user-static via libc6-amd64-cross
ronaldtse Jul 26, 2026
415d1a7
Switch to Debian multiarch for full x86_64 userland under qemu
ronaldtse Jul 26, 2026
5651340
Run OHOS build inside amd64 docker container (simpler than multiarch)
ronaldtse Jul 26, 2026
1a32b02
Symlink LLVM-19 sysroot over SDK's partial sysroot
ronaldtse Jul 26, 2026
f16fcf2
Fix LLVM-19 sysroot extraction: tarball already has sysroot/ at top
ronaldtse Jul 26, 2026
c857f1a
Add curl retry logic for OHOS CDN downloads
ronaldtse Jul 26, 2026
d733557
Dump actual llvm-19 layout to find where sysroot landed
ronaldtse Jul 26, 2026
d689c8d
Symlink to per-arch sysroot (aarch64-linux-ohos), not multiarch root
ronaldtse Jul 26, 2026
5e421cb
Add diagnostics: check CRT objects + libs in both sysroots
ronaldtse Jul 26, 2026
8b7e0a3
Resolve PREFIX to absolute path so ln -s creates valid symlinks
ronaldtse Jul 26, 2026
744dbd2
mkdir PREFIX before resolving to absolute path
ronaldtse Jul 26, 2026
d7eb4ae
Use relative symlink target so it resolves in docker container too
ronaldtse Jul 26, 2026
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
119 changes: 92 additions & 27 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +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 +168,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 +187,90 @@ jobs:
- uses: actions/upload-artifact@v4
with:
name: pkg-${{ matrix.platform }}
path: pkg/*.gem

# OHOS (OpenHarmony / Huawei HarmonyOS PC) requires the official OHOS
# NDK to cross-compile. The NDK's clang and binary-sign-tool are x86_64
# ELF binaries; on this arm64 runner they run transparently via
# binfmt_misc + qemu-user-static. Verification (dockerharmony) runs
# natively because the arm64 rootfs matches the host architecture.
build_ohos:
name: build aarch64-linux-ohos (NDK)
runs-on: ubuntu-24.04-arm
needs: prepare
steps:
- uses: actions/checkout@v4

- name: Install host-side tools
# These are used for: NDK setup (curl/jq/unzip), artifact prep
# (file), and dockerharmony verification (docker is preinstalled).
run: |
sudo apt-get update
sudo apt-get install -y curl jq unzip file

- name: Register binfmt for amd64 docker emulation
# The OHOS NDK is x86_64-only. We run the entire build inside an
# amd64 docker container on this arm64 host -- docker's binfmt_misc
# registration handles the emulation transparently inside the
# container, with no library juggling on the host side.
run: docker run --privileged --rm tonistiigi/binfmt --install amd64

- name: Cache OHOS NDK
id: cache-ndk
uses: actions/cache@v4
with:
path: ext/ohos/ndk
key: ohos-ndk-${{ hashFiles('ext/ohos/setup-ndk.sh') }}

- name: Setup OHOS NDK (host-side; only curl/jq/unzip needed)
if: steps.cache-ndk.outputs.cache-hit != 'true'
run: sh ext/ohos/setup-ndk.sh --prefix ext/ohos/ndk

- name: Build OHOS gem in amd64 container
# The entire build (libpng + static zlib + signing) runs inside an
# amd64 docker container so the NDK's x86_64 clang + binary-sign-tool
# run natively (no qemu at the process level; docker's binfmt handles
# the container emulation as a whole).
run: |
docker run --rm --platform linux/amd64 \
-v "$PWD:/work" -w /work \
-e target_platform=aarch64-linux-ohos \
ruby:3.3 \
sh -c '
set -e
apt-get update
apt-get install -y --no-install-recommends cmake ninja-build zlib1g-dev
bundle install --jobs 4
bundle exec rake gem:native:aarch64-linux-ohos
# Confirm the .so was produced and is arm64 ELF (sanity).
file lib/libpng/libpng16.so
'

- name: Prepare dockerharmony artifacts (host-side)
# verify-prepare.sh cross-compiles smoke-test.c using the NDK clang.
# We run it inside the same amd64 container for the same reason.
run: |
docker run --rm --platform linux/amd64 \
-v "$PWD:/work" -w /work \
-e NDK_ROOT=/work/ext/ohos/ndk \
ruby:3.3 \
sh -c '
set -e
apt-get update
apt-get install -y --no-install-recommends file
sh ext/ohos/verify-prepare.sh
'

- name: Verify in dockerharmony (real OHOS userland, arm64-native)
run: |
docker pull ghcr.io/hqzing/dockerharmony:latest
docker run --rm \
-v "$PWD/ohos-verify:/work" \
-w /work \
ghcr.io/hqzing/dockerharmony:latest \
sh -c 'LD_LIBRARY_PATH=. ./smoke-test'

- uses: actions/upload-artifact@v4
with:
name: pkg-aarch64-linux-ohos
path: pkg/*.gem
95 changes: 74 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,79 @@ jobs:
name: pkg-${{ matrix.platform }}
path: pkg/*.gem

# OHOS NDK cross-compile. See build.yml's build_ohos job for full notes.
build_ohos:
needs: bump
if: always() && !cancelled() && !failure() && (needs.bump.result == 'success' || needs.bump.result == 'skipped')
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.bump.outputs.sha || github.ref }}

- name: Install host-side tools
run: |
sudo apt-get update
sudo apt-get install -y curl jq unzip file

- name: Register binfmt for amd64 docker emulation
run: docker run --privileged --rm tonistiigi/binfmt --install amd64

- name: Cache OHOS NDK
id: cache-ndk
uses: actions/cache@v4
with:
path: ext/ohos/ndk
key: ohos-ndk-${{ hashFiles('ext/ohos/setup-ndk.sh') }}

- name: Setup OHOS NDK
if: steps.cache-ndk.outputs.cache-hit != 'true'
run: sh ext/ohos/setup-ndk.sh --prefix ext/ohos/ndk

- name: Build OHOS gem in amd64 container
run: |
docker run --rm --platform linux/amd64 \
-v "$PWD:/work" -w /work \
-e target_platform=aarch64-linux-ohos \
ruby:3.3 \
sh -c '
set -e
apt-get update
apt-get install -y --no-install-recommends cmake ninja-build zlib1g-dev
bundle install --jobs 4
bundle exec rake gem:native:aarch64-linux-ohos
file lib/libpng/libpng16.so
'

- name: Prepare dockerharmony artifacts
run: |
docker run --rm --platform linux/amd64 \
-v "$PWD:/work" -w /work \
-e NDK_ROOT=/work/ext/ohos/ndk \
ruby:3.3 \
sh -c '
set -e
apt-get update
apt-get install -y --no-install-recommends file
sh ext/ohos/verify-prepare.sh
'

- name: Verify in dockerharmony (real OHOS userland)
run: |
docker pull ghcr.io/hqzing/dockerharmony:latest
docker run --rm \
-v "$PWD/ohos-verify:/work" \
-w /work \
ghcr.io/hqzing/dockerharmony:latest \
sh -c 'LD_LIBRARY_PATH=. ./smoke-test'

- 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
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,56 @@ 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 official OHOS
NDK** (Huawei's `ohos.toolchain.cmake` + LLVM-19 clang), replacing the
1.6.58.4/.5 approach of shipping Alpine-built musl bytes labeled as OHOS.

The build runs on `ubuntu-24.04-arm`. The OHOS NDK clang is an x86_64
binary and runs transparently via `binfmt_misc` + `qemu-user-static`.
zlib is built statically with the same toolchain and linked into
`libpng16.so` (avoids the OHOS non-standard zlib SONAME
`libshared_libz.z.so`). The freshly built `.so` is code-signed with
`binary-sign-tool sign -selfSign 1` (mandatory for runtime loading on
production OHOS devices).

Verification: the signed `.so` is loaded by a smoke-test binary inside
real OHOS userland (the `dockerharmony` container running natively on
the same arm64 host -- no qemu). Build fails if the smoke test fails.

### Added
- `lib/libpng/ohos.rb`, `lib/libpng/ohos/{ndk,zlib_builder,code_signer,recipe}.rb`
-- OHOS cross-compile support. Lazy-loaded only when building for OHOS.
- `Libpng::OHOS::NDK` -- pure-data class for NDK path discovery.
- `Libpng::OHOS::ZlibBuilder < MiniPortileCMake` -- static zlib build.
- `Libpng::OHOS::CodeSigner` -- wraps `binary-sign-tool`.
- `Libpng::OHOS::Recipe < Libpng::Recipe` -- orchestrates the OHOS build.
- `lib/libpng/recipe.rb`: `Recipe.for_target(platform)` factory -- OCP seam
that returns `OHOS::Recipe` for `*-ohos` targets, base `Recipe` otherwise.
- `ext/ohos/setup-ndk.sh` -- downloads OHOS SDK + LLVM-19 via daily_build API
(adapted from ohos-node's `build.sh`).
- `ext/ohos/smoke-test.c` -- minimal libpng round-trip test for dockerharmony.
- `ext/ohos/verify-prepare.sh` -- cross-compiles smoke-test with NDK clang,
bundles signed `.so` + SONAME symlinks (fixes PR #12's symlink bug).
- 49 new specs under `spec/ohos/` + `spec/recipe_factory_spec.rb`.

### Caveats
- The 1.6.58.4/.5 OHOS gems shipped Alpine-built bytes without NDK
verification. This version is the first with proper OHOS NDK
cross-compilation + signing.
- Code signing uses `-selfSign 1` (self-signed). Sufficient for OHOS
userland load. Production deployment on Huawei-managed hardware may
require re-signing with an enrolled cert.

### Superseded
- PR #11 (closed) -- hand-written `toolchain.cmake` + system CMake. Replaced
by NDK's bundled `ohos.toolchain.cmake`.
- PR #12 (closed) -- dockerharmony verification of Alpine bytes. The
verification step is reused here; the Alpine-built `.so` is replaced
with the NDK-built, signed one.

## [1.6.58.5] - 2026-07-26

### Added
Expand Down
31 changes: 29 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,16 @@ 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) |
| `ext/extconf.rb` | Gem extension entry. Triggers `Libpng::Recipe` autoload via `require 'libpng'`, then emits a dummy Makefile |
| `lib/libpng/recipe.rb` | `Libpng::Recipe < MiniPortileCMake` (builds libpng from source for the source gem). Has `.for_target(platform)` factory -- OCP seam that returns `OHOS::Recipe` for `*-ohos`, base `Recipe` otherwise |
| `lib/libpng/ohos.rb` | `Libpng::OHOS` namespace + autoloads. Lazy-loaded only when `Recipe.for_target` is called with an OHOS target |
| `lib/libpng/ohos/ndk.rb` | `OHOS::NDK` -- pure-data class for NDK path discovery (toolchain/sysroot/clang/sign-tool). Idempotent `#download` via `setup-ndk.sh` |
| `lib/libpng/ohos/zlib_builder.rb` | `OHOS::ZlibBuilder < MiniPortileCMake` -- builds `libz.a` statically with the OHOS toolchain |
| `lib/libpng/ohos/code_signer.rb` | `OHOS::CodeSigner` -- wraps `binary-sign-tool sign -selfSign 1`. Pure-data `#sign_command` is testable without invoking the tool |
| `lib/libpng/ohos/recipe.rb` | `OHOS::Recipe < Libpng::Recipe` -- orchestrates NDK setup, zlib build, libpng cross-compile with `ohos.toolchain.cmake`, and post-install signing |
| `ext/extconf.rb` | Gem extension entry. Calls `Libpng::Recipe.for_target(ENV['target_platform']).new`, then emits a dummy Makefile |
| `ext/ohos/setup-ndk.sh` | Downloads OHOS SDK + LLVM-19 via OpenHarmony daily_build API (adapted from ohos-node/build.sh). Idempotent |
| `ext/ohos/smoke-test.c` | Minimal libpng round-trip test, cross-compiled with NDK clang and run inside dockerharmony |
| `ext/ohos/verify-prepare.sh` | Cross-compiles smoke-test, bundles signed `.so` + SONAME symlinks for dockerharmony |

### Public API

Expand Down Expand Up @@ -74,6 +82,25 @@ bundle exec rake gem:native:any # source gem (compiles on install)

Platform gem tasks: `x64-mingw32`, `x64-mingw-ucrt`, `aarch64-mingw-ucrt`, `x86_64-linux`, `x86_64-linux-musl`, `aarch64-linux`, `aarch64-linux-musl`, `aarch64-linux-ohos`, `x86_64-darwin`, `arm64-darwin`.

### OHOS (`aarch64-linux-ohos`) build notes

OHOS uses a dedicated `build_ohos` CI job (see `.github/workflows/build.yml`)
that runs on `ubuntu-24.04-arm`. The NDK's clang and `binary-sign-tool` are
x86_64 ELF and run via `binfmt_misc` + `qemu-user-static`. The job:

1. Downloads OHOS SDK + LLVM-19 via `ext/ohos/setup-ndk.sh` (~1.5 GB, cached
across runs by `actions/cache@v4`).
2. Runs `rake gem:native:aarch64-linux-ohos`, which delegates to
`Libpng::OHOS::Recipe` (sets up NDK, builds static zlib, cross-compiles
libpng with `ohos.toolchain.cmake`, signs the `.so`).
3. Cross-compiles `ext/ohos/smoke-test.c` with NDK clang via
`verify-prepare.sh`, then runs it inside the `dockerharmony` container
(real OHOS userland) — build fails if smoke-test doesn't output `OK`.

Local dev: `target_platform=aarch64-linux-ohos bundle exec rake compile`
will only succeed on an arm64 host with `qemu-user-static` registered,
because the NDK clang is x86_64 ELF. CI handles this automatically.

## Release process

Releases are tag-triggered via `.github/workflows/release.yml`. Trigger with:
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-24.04-arm` + OHOS NDK (cross-compiled, signed, **verified in dockerharmony**)
| `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
Loading
Loading