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
29 changes: 29 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,13 @@ jobs:
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')

# Build the cross-platform smoke test that dockerharmony will
# run. Compiled here (in Alpine, where we have gcc + libpng
# headers) and run in the next workflow step inside the
# dockerharmony container (real OHOS userland).
apk add --no-cache gcc musl-dev
sh /work/ext/ohos/verify-prepare.sh
;;
*)
gem install -b pkg/libpng-*.gem
Expand All @@ -205,6 +212,28 @@ jobs:
ruby:${RUBY_VERSION}-alpine \
/alpine-build.sh

# OHOS verification: real userland test via dockerharmony.
# The Alpine-built .so + smoke-test binary are aarch64-linux-musl,
# which the dockerharmony image's OHOS rootfs runs natively.
# If this passes, we have empirical proof the bytes work in OHOS,
# not just an assumption based on matching dynamic linker paths.
- name: Verify in dockerharmony (OHOS userland)
if: matrix.platform == 'aarch64-linux-ohos'
shell: bash
run: |
# Pull the dockerharmony image (cached on the arm64 runner).
docker pull ghcr.io/hqzing/dockerharmony:latest

# Run the smoke-test binary + .so inside the OHOS userland.
# LD_LIBRARY_PATH=. lets the binary find libpng16.so + libz.so
# next to itself (rpath=$ORIGIN also helps but doesn't always
# work for transitive deps under musl).
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-${{ matrix.platform }}
Expand Down
18 changes: 18 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,10 @@ jobs:
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')

# Cross-platform smoke test that dockerharmony will run.
apk add --no-cache gcc musl-dev
sh /work/ext/ohos/verify-prepare.sh
;;
*)
gem install -b pkg/libpng-*.gem
Expand All @@ -248,6 +252,20 @@ jobs:
ruby:3.3-alpine \
/alpine-build.sh

# OHOS verification: real userland test via dockerharmony.
# The Alpine-built .so + smoke-test binary are aarch64-linux-musl,
# which the dockerharmony image's OHOS rootfs runs natively.
- name: Verify in dockerharmony (OHOS userland)
if: matrix.platform == 'aarch64-linux-ohos'
shell: bash
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-${{ matrix.platform }}
Expand Down
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,52 @@ 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`) verification is now done in real OHOS
userland** via the [`dockerharmony`](https://github.com/hqzing/dockerharmony)
container, rather than relying on the unverified assumption that
Alpine-built musl arm64 bytes are byte-compatible with OHOS.

The build itself is unchanged from 1.6.58.4/.5: libpng is built in
the Alpine container on an arm64 runner, producing an aarch64-linux-musl
binary. What's new is the verification step -- the freshly built `.so`
is now copied into a dockerharmony container (real OHOS mini rootfs:
musl + toybox + mksh), a smoke-test binary that round-trips a PNG
encode/decode runs there, and the build only passes if the smoke test
succeeds inside OHOS userland.

This replaces the OHOS NDK cross-compile attempt (PR #11, closed)
that was hitting repeated issues with sysroot layout, missing zlib
in the OHOS sysroot, and CMake cross-compile quirks. The dockerharmony
path is much simpler -- no NDK download, no toolchain file, no
cross-compile -- AND provides stronger empirical verification (real
OHOS userland vs qemu emulation).

### Added
- `ext/ohos/smoke-test.c` -- minimal libpng round-trip test (encode ->
decode -> byte comparison).
- `ext/ohos/verify-prepare.sh` -- compiles the smoke test against the
freshly built `.so` inside the Alpine container, producing a tarball
of artifacts (smoke-test binary + libpng16.so + libz.so) for
dockerharmony to run.
- New `Verify in dockerharmony (OHOS userland)` step in `build.yml`
and `release.yml`'s `build_musl` matrix, gated on `platform ==
'aarch64-linux-ohos'`.

### Caveats
- The 1.6.58.4/.5 OHOS gems shipped the same Alpine-built bytes WITHOUT
the dockerharmony verification. Those versions may or may not have
worked on real OHOS hardware; this version is the first with empirical
CI evidence that the bytes load and round-trip in OHOS userland.
- Code signing is still NOT applied. OHOS requires code signing for
runtime loading on real devices; dockerharmony skips this check (it's
a dev container). Users running on actual OHOS hardware may need to
sign the `.so` themselves (`binary-sign-tool sign -selfSign 1`) before
it will load. A future release may add automatic signing if a real
OHOS hardware user reports breakage.

## [1.6.58.5] - 2026-07-26

### Added
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ loaded via `autoload` from `lib/libpng.rb`. **Never use `require_relative`
| `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 |
| `ext/ohos/smoke-test.c` | Minimal libpng round-trip test, run inside the dockerharmony container to verify OHOS compatibility |
| `ext/ohos/verify-prepare.sh` | Compiles `smoke-test.c` against the freshly built `.so` (in Alpine), producing artifacts dockerharmony consumes |

### 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 | `ruby:<ver>-alpine` on arm64, **verified in dockerharmony** (real OHOS userland)
| `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
73 changes: 73 additions & 0 deletions ext/ohos/smoke-test.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* Minimal libpng round-trip test, run inside the dockerharmony
* container (real OHOS userland) to verify that an Alpine-built
* libpng16.so actually loads and works on OHOS.
*
* The binary itself is also built under Alpine (aarch64-linux-musl),
* then copied into dockerharmony along with the .so. Both run via
* the OHOS dynamic linker (/lib/ld-musl-aarch64.so.1).
*
* This is the empirical check that "Alpine-built musl arm64 bytes
* work in real OHOS userland" -- not just an assumption based on
* matching dynamic-linker paths.
*/
#include <png.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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;
}

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);

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);

if (memcmp(pixels, out_pixels, 16) != 0) {
fprintf(stderr, "pixel mismatch\n");
return 1;
}

printf("OK\n");
return 0;
}
58 changes: 58 additions & 0 deletions ext/ohos/verify-prepare.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/bin/sh
# Verifies that an Alpine-built libpng16.so actually loads and
# round-trips in the dockerharmony container (real OHOS userland).
#
# Runs inside the Alpine container as a continuation of the regular
# aarch64-linux-ohos build. Steps:
# 1. Compile ext/ohos/smoke-test.c against the freshly-built .so
# (using Alpine's gcc, since both produce aarch64-linux-musl
# binaries that run in dockerharmony).
# 2. Bundle the .so + smoke binary + libz.so into a tarball that
# dockerharmony can run.
#
# The dockerharmony run itself happens in the workflow YAML (host
# side), not here -- this script just produces the artifacts.
set -e

WORK="${WORK:-$(pwd)}"
SO_PATH="$WORK/lib/libpng/libpng16.so"
SMOKE_SRC="$WORK/ext/ohos/smoke-test.c"

if [ ! -f "$SO_PATH" ]; then
echo "verify-prepare: $SO_PATH not found; did rake compile run?" >&2
exit 1
fi

# libpng headers shipped with the source tree (mini_portile installed
# them under ports/...). Fallback to system libpng-dev headers if not.
LIBPNG_VERSION=$(ruby -I"$WORK/lib" -rlibpng/version -e 'puts Libpng::LIBPNG_VERSION')
INCLUDES="-I$WORK/ports/ports/libpng-${LIBPNG_VERSION}/include"
if [ ! -d "$WORK/ports/ports/libpng-${LIBPNG_VERSION}/include" ]; then
INCLUDES="" # rely on libpng-dev installed via apk
fi

# Output dir on the host (mounted volume).
OUT="/work/ohos-verify"
mkdir -p "$OUT"

# Compile the smoke test. -Wl,-rpath=$ORIGIN means the binary finds
# the .so in its own dir at runtime. We also link zlib statically
# where possible (dockerharmony may not have libz.so).
gcc -O2 $INCLUDES \
-L"$WORK/lib/libpng" \
-Wl,-rpath=\$ORIGIN \
-o "$OUT/smoke-test" \
"$SMOKE_SRC" \
-lpng16 -lz -lm

# Copy the .so next to the smoke binary so rpath=$ORIGIN resolves.
cp "$SO_PATH" "$OUT/libpng16.so"

# Also copy libz.so in case dockerharmony doesn't ship it.
LIBZ_PATH=$(ldd "$SO_PATH" | grep 'libz\.so' | awk '{print $3}')
if [ -n "$LIBZ_PATH" ] && [ -f "$LIBZ_PATH" ]; then
cp "$LIBZ_PATH" "$OUT/libz.so"
fi

echo "verify-prepare: artifacts in $OUT"
ls -la "$OUT"
2 changes: 1 addition & 1 deletion lib/libpng/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading