OHOS: cross-compile with NDK + sign + verify in dockerharmony (1.6.58.6) - #13
Merged
Conversation
12 design documents covering the architecture, NDK setup, zlib build, recipe factory, code signing, smoke-test, CI workflow, specs, docs, and PR plan for cross-compiling libpng for OHOS with the official NDK. Replaces PR #12's Alpine-built bytes labeled OHOS approach with proper OHOS NDK cross-compile + signing + dockerharmony verification.
Adds the Ruby + shell infrastructure to cross-compile libpng for aarch64-linux-ohos using Huawei's official OHOS NDK: - ext/ohos/setup-ndk.sh: idempotent download of OHOS SDK + LLVM-19 via the daily_build API (pattern adapted from ohos-node/build.sh). - ext/ohos/smoke-test.c: minimal libpng round-trip test (encode -> decode -> byte compare), cross-compiled with NDK clang and run inside dockerharmony for empirical OHOS userland verification. - ext/ohos/verify-prepare.sh: cross-compiles smoke-test with NDK clang, bundles the signed .so + SONAME symlinks. Fixes the symlink bug from PR #12 where only libpng16.so (dev symlink) was bundled but the smoke-test binary's NEEDED entry was libpng16.so.16 (SONAME). - lib/libpng/ohos.rb: namespace + autoloads for OHOS::NDK, OHOS::Recipe, OHOS::ZlibBuilder, OHOS::CodeSigner. - lib/libpng/ohos/ndk.rb: pure-data class for NDK path discovery (toolchain, sysroot, clang, sign-tool). No shell calls except via explicit #download (invokes setup-ndk.sh). - lib/libpng/ohos/zlib_builder.rb: OHOS::ZlibBuilder < MiniPortileCMake. Builds zlib statically (libz.a) with the OHOS toolchain; linked into libpng16.so to avoid OHOS's non-standard libshared_libz.z.so dependency. - 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 the OHOS build: download NDK, build static zlib, configure libpng with ohos.toolchain.cmake + static zlib, sign the .so post-install. - lib/libpng/recipe.rb: adds Recipe.for_target(platform) class method factory. OCP-compliant seam: returns OHOS::Recipe for *-ohos targets, base Recipe otherwise. Existing recipe logic unchanged. - lib/libpng.rb: autoload :OHOS entry added (lazy-loaded only when OHOS target is requested). - ext/extconf.rb: switches to Recipe.for_target(...).new so the source gem (no target_platform) still uses base Recipe. Zlib is built statically because libpng's CMake find_package(ZLIB) expects libz.so and OHOS ships zlib under SONAME libshared_libz.z.so. Static linking sidesteps the whole problem. Code-signing uses -selfSign 1 (self-signed) which is sufficient for OHOS userland load. Production Huawei-managed deployments may require re-signing with an enrolled cert; documented in CHANGELOG caveats. Architecture decision: run on ubuntu-24.04-arm (native arm64) with binfmt_misc + qemu-user-static. The NDK clang is x86_64 ELF; on an arm64 host it runs transparently via binfmt. Verification (dockerharmony) runs natively because the arm64 rootfs matches the host arch. 49 new specs under spec/ohos/ + spec/recipe_factory_spec.rb covering NDK path discovery, factory wiring, ZlibBuilder configure_defaults, CodeSigner command shape, and OHOS::Recipe configure_defaults. No doubles used (real instances + tmpdir-based fake NDK structure).
Removes the aarch64-linux-ohos entry from build_musl (no longer Alpine-built) and adds a dedicated build_ohos job in both build.yml and release.yml. The new job runs on ubuntu-24.04-arm and: 1. Installs qemu-user-static + binfmt-support and registers binfmt for x86_64 (NDK binaries are x86_64 ELF). 2. Caches ext/ohos/ndk/ across runs (~1.5 GB; key includes setup-ndk.sh hash so cache invalidates when the script changes). 3. Runs setup-ndk.sh if cache miss. 4. Builds the OHOS gem (libpng + static zlib + signing) via rake gem:native:aarch64-linux-ohos with target_platform env var set. 5. Runs verify-prepare.sh to cross-compile smoke-test for dockerharmony. 6. Pulls dockerharmony and runs the smoke-test in real OHOS userland. Build fails if smoke-test doesn't output OK. publish job (release.yml) now waits on [bump, build, build_musl, build_ohos] so the OHOS gem is included in every release.
- lib/libpng/version.rb: LIBPNG_RUBY_ITERATION 5 -> 6. - CHANGELOG.md: new 1.6.58.6 entry describing the OHOS NDK cross-compile (toolchain, static zlib, signing, dockerharmony verify) and listing PR #11 and #12 as superseded. - README.adoc: aarch64-linux-ohos row now reflects the actual build host (ubuntu-24.04-arm + OHOS NDK, verified in dockerharmony) instead of the misleading "Alpine, byte-compatible with musl". - CLAUDE.md: file table extended with lib/libpng/ohos/ + ext/ohos/; Common commands section now has an "OHOS build notes" subsection explaining the binfmt_misc + qemu-user-static setup and that local builds require arm64 + binfmt registration.
Run 1 failed: "cd: can't cd to ext/ohos/ndk/ohos-sdk/linux".
The ohos-sdk-public tarball extracts to ./ohos-sdk/{linux,windows,ohos}/
at the top level (per ohos-node/build.sh). Extracting into
$PREFIX/ohos-sdk created a nested $PREFIX/ohos-sdk/ohos-sdk/linux/.
Fix: extract straight into $PREFIX so the path lands at
$PREFIX/ohos-sdk/linux/ as expected. Same pattern for LLVM-19.
Run 2 failed because OHOS::NDK#exist? returned false even though
setup-ndk.sh reported "NDK ready". Likely my hardcoded paths
(ohos-sdk/linux/{build/cmake/ohos.toolchain.cmake,
toolchains/lib/binary-sign-tool}) don't match the actual extracted
layout. The OHOS CMake doc suggests paths include a native/ prefix
(SDK_PATH/native/build/cmake/ohos.toolchain.cmake).
Add a find + ls at the end of setup-ndk.sh to print the actual
locations of ohos.toolchain.cmake, binary-sign-tool, and clang.
Once we see the real layout we can fix the NDK class paths.
Run 3 find output revealed ohos.toolchain.cmake is NOT in the daily_build ohos-sdk-public tarball (only binary-sign-tool + clang were found). Two possible causes: 1. The SDK ships native-*.zip separately and ohos-node only unzipped toolchains-*.zip. Fix: change the unzip loop from 'toolchains-*.zip' to '*.zip' so any native-*.zip gets extracted too. 2. The toolchain file lives under ohos-sdk/linux/native/build/cmake/ (per the OHOS CMake doc, which uses SDK_PATH/native/build/cmake/). Fix: update OHOS::NDK#toolchain_path and #cmake_path to include the native/ prefix. Both fixes are pushed together. If native/ doesn't appear after this, the find diagnostics will show what's actually there so we can adapt (fallback: write our own minimal toolchain.cmake or use system clang with --target=aarch64-linux-ohos). Specs updated to match the new native/ paths.
Run 4 failed at the very first try_compile with: x86_64-binfmt-P: Could not open '/lib64/ld-linux-x86-64.so.2' Root cause: 'update-binfmts --enable qemu-x86_64' registers qemu as the handler for x86_64 ELF but WITHOUT the fix-binary (F) flag. Without F, when the kernel tries to exec an x86_64 DYNAMIC binary, it tries to use the host's /lib64/ld-linux-x86-64.so.2 interpreter (which doesn't exist on arm64 Ubuntu) instead of the guest's interpreter embedded by qemu. Switch to 'docker run --privileged --rm tonistiigi/binfmt --install amd64' which registers binfmt_misc with the F flag. This embeds qemu into the kernel's binfmt handlers at registration time, so dynamic x86_64 binaries run transparently on the arm64 host. Also strengthen the smoke check: compile + run a static x86_64 hello-world binary INSIDE an amd64 docker container, then run it on the host file system. If host binfmt is broken, the binary won't run. Drop qemu-user-static + binfmt-support from apt install since tonistiigi/binfmt installs its own qemu binaries.
The smoke check compiled a hello-world inside an alpine:amd64 container to verify host binfmt was wired up. But apk's gcc package on Alpine doesn't pull in libc-dev, so stdio.h is missing and the smoke check itself failed. Drop the smoke check entirely -- the NDK clang invocation in the next step is the real test. If binfmt is broken, the build fails clearly with the same x86_64-binfmt-P error we saw in run 4.
Run 6 still failed with 'qemu-x86_64: Could not open /lib64/ld-linux-x86-64.so.2' even after switching to tonistiigi/binfmt. The F flag registers qemu as the handler, but qemu itself still needs to find the x86_64 dynamic loader to exec dynamic binaries. tonistiigi/binfmt doesn't install an x86_64 rootfs. Install libc6-amd64-cross (which provides the loader at /usr/x86_64-linux-gnu/lib/ld-linux-x86-64.so.2) and symlink it to /lib64/ld-linux-x86-64.so.2 (the path qemu looks for).
Run 7 got past the loader error but then hit 'cannot open shared object file: libpthread.so.0'. libc6-amd64-cross only ships libc -- not the libstdc++/libpthread/libgcc_s the NDK clang needs. Debian multiarch installs the full x86_64 userland alongside arm64: dpkg --add-architecture amd64 + apt install libc6:amd64 libstdc++6:amd64 libgcc-s1:amd64. The packages install at /lib/x86_64-linux-gnu/ with the loader symlinked at /lib64/ld-linux-x86-64.so.2.
Runs 5-8 tried to make the arm64 host run x86_64 NDK binaries directly via qemu-user-static. Each fix revealed another missing piece: - update-binfmts lacks F flag -> tonistiigi/binfmt - qemu needs x86_64 loader -> libc6-amd64-cross symlink - clang needs libpthread/libstdc++ -> Debian multiarch - ports.ubuntu.com has no binary-amd64 -> need to add archive.ubuntu.com source Each step added complexity. Stepping back: docker's binfmt_misc registration is rock-solid (run 5 confirmed docker emulation works on this runner). Move the entire build INSIDE an amd64 docker container. Docker handles the amd64 emulation as a whole, with no host-side library juggling. New topology: - Runner: ubuntu-24.04-arm (arm64 native, satisfies the 'OHOS runs on ARM' framing) - Build: docker run --platform linux/amd64 ruby:3.3 -> NDK clang runs natively x86_64 inside the container - Verify: docker run ghcr.io/hqzing/dockerharmony -> arm64 native (no emulation) Trade-off: ~3-5x slowdown for the build phase (qemu emulates the entire container), but reliability >> speed here. The build phase takes ~1-2 min natively, so ~5-10 min under emulation is acceptable. NDK cache key drops the 'arm64' suffix since the cached NDK bytes are architecture-independent (just downloaded + extracted).
Run 9 got the toolchain + clang working but failed at:
ohos-sdk/linux/native/sysroot/usr/include/limits.h:6: fatal error:
'bits/alltypes.h' file not found
The SDK bundles a partial sysroot at ohos-sdk/linux/native/sysroot/
(missing arch-specific musl headers). The LLVM-19 tarball we also
download ships ohos-sysroot.tar.gz with the complete sysroot.
Replace the SDK's partial sysroot with a symlink to the LLVM-19 sysroot
so ohos.toolchain.cmake (which resolves CMAKE_SYSROOT relative to its
own location) picks up the complete headers.
Add diagnostics to confirm the symlink took and that alltypes.h is now
reachable from both sysroot paths.
Run 10 diagnostic showed llvm-19/sysroot/usr/include/ doesn't exist. My 'mkdir sysroot && tar -C sysroot -zxf ohos-sysroot.tar.gz' created a nested llvm-19/sysroot/sysroot/usr/include/ instead. ohos-sysroot.tar.gz already contains sysroot/ at the top level (per ohos-node/build.sh which extracts with no -C). Drop the mkdir + -C and extract directly into llvm-19/.
Run 11 failed at the SDK download step with: curl: (35) Recv failure: Connection reset by peer cidownload.openharmony.cn sometimes resets connections mid-transfer (observed on runs 1-10 too, but always succeeded on retry). Add --retry 5 --retry-delay 10 --retry-all-errors to both the API query and the tarball downloads so transient network issues don't fail the build.
Run 12 still failed with 'bits/alltypes.h not found' AND diagnostic showed llvm-19/sysroot/usr/include/ doesn't exist. My fix (drop the mkdir + -C sysroot) didn't work, which means ohos-sysroot.tar.gz has yet another top-level layout. Add diagnostics: - ls llvm-19/ (top-level) - find llvm-19/ -maxdepth 2 -type d (the dir tree) - find anywhere for alltypes.h or 'bits' dir - tar -tzf on LLVM-19.tar.gz to see what the inner sysroot tarball contains This will tell us the actual layout so we can extract correctly.
Run 13 diagnostics revealed the layout mismatch: - SDK sysroot at ohos-sdk/linux/native/sysroot/usr/include/ uses MULTIARCH layout: usr/include/<arch>/bits/alltypes.h - LLVM-19 sysroot at llvm-19/sysroot/<arch>/usr/include/ uses PER-ARCH layout: <arch>/usr/include/bits/alltypes.h The OHOS toolchain expects per-arch layout (one sysroot per arch with plain usr/include/bits/ inside). My earlier symlink pointed at the LLVM-19 multiarch root, which still didn't have usr/include/bits/ at the expected path. Fix: point the symlink at llvm-19/sysroot/aarch64-linux-ohos/ specifically (per-arch subdir). Then ohos-sdk/linux/native/sysroot/usr/include/bits/ alltypes.h resolves correctly to the LLVM-19 per-arch sysroot.
Run 14 got past header errors but linker fails: ld.lld: cannot open Scrt1.o ld.lld: cannot open crti.o ld.lld: unable to find library -lm ld.lld: unable to find library -lc Possible cause: my symlink to llvm-19/sysroot/aarch64-linux-ohos/ points at a sysroot that has headers but no libraries. The original SDK sysroot (multiarch) had the libraries. Add diagnostics to see where Scrt1.o, crti.o, libc.so, libm.so actually live. If they're only in the multiarch SDK sysroot, we may need to merge both sysroots instead of replacing one with the other.
Run 15 diagnostic showed the symlink wasn't being created correctly: setup-ndk: ohos-sdk/linux/native/sysroot resolves to: ls: cannot access 'ext/ohos/ndk/ohos-sdk/linux/native/sysroot/usr/lib' Root cause: $PREFIX was passed as 'ext/ohos/ndk' (relative) via the workflow's --prefix arg. ln -s interprets relative targets relative to the SYMLINK's location, not the script's cwd. So the symlink at ext/ohos/ndk/ohos-sdk/linux/native/sysroot pointed at ext/ohos/ndk/llvm-19/sysroot/aarch64-linux-ohos -- which from the symlink's dir resolves to ext/ohos/ndk/ohos-sdk/linux/native/ext/ohos/ndk/llvm-19/... (dangling). Fix: resolve PREFIX to an absolute path with $(cd "$PREFIX" && pwd) right after parsing args. Now ln -s writes an absolute target and the symlink resolves correctly regardless of cwd.
Run 16 failed at 'cd: can't cd to ext/ohos/ndk' on the new absolute-path resolution line. The dir doesn't exist yet at that point -- it was being created later in the script. Move mkdir earlier.
Run 17 diagnostic confirmed the symlink works on the host: SDK sysroot is symlink to: /home/runner/work/libpng-ruby/libpng-ruby/ext/ohos/ndk/llvm-19/sysroot/aarch64-linux-ohos But the BUILD runs inside a docker container that mounts the repo at /work/, not /home/runner/.... The absolute symlink target dangles inside the container, so clang's --sysroot path appears empty and ld.lld can't find Scrt1.o, libc.so, etc. Fix: use a relative symlink target (../../../llvm-19/sysroot/aarch64- linux-ohos). The relative path from ohos-sdk/linux/native/ to llvm-19/sysroot/aarch64-linux-ohos is the same regardless of where the repo is mounted.
Contributor
Author
|
For future porting work, I wrote up the OHOS NDK cross-compile + verification pattern as a public gist: Porting C libraries and Ruby platform gems to OHOS (OpenHarmony) Covers everything learned across the 18 CI iterations of this PR:
Generalizes to any C library that uses CMake. Reference: this PR. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Closes #14.
Replace the 1.6.58.4/.5 "Alpine-built bytes labeled as OHOS" approach with a proper OHOS NDK cross-compile. The shipped
.sois now built with Huawei's officialohos.toolchain.cmake, statically linked against zlib built with the same toolchain, code-signed withbinary-sign-tool, and empirically verified in real OHOS userland (dockerharmony).Supersedes #12 (closed; shipped Alpine bytes with dockerharmony verification but no NDK build) and #11 (closed; 5 CI iterations failed because of a hand-written toolchain.cmake instead of NDK's bundled one).
Why
The 1.6.58.4/.5 OHOS gems shipped Alpine-built musl arm64 bytes under the OHOS platform label, based on an unverified assumption that "same dynamic linker path = byte-compatible". Real risks flagged: musl symbol versions differ, -fvisibility=hidden defaults differ, TLS/pthread layout may differ.
PR #11 attempted to fix this via OHOS NDK cross-compile but used a hand-written
toolchain.cmakewith system CMake. After 5 CI iterations it still couldn't get past try_compile/sysroot/zlib-detection issues.PR #12 added dockerharmony verification of the Alpine bytes — proving they load in OHOS userland, but not actually fixing the underlying "wrong ABI" concern.
This PR (#13) does it properly:
ohos.toolchain.cmake(the official Huawei CMake integration, which handles sysroot + clang target + try_compile).libshared_libz.z.so, notlibz.so— static linking sidesteps this)..sowithbinary-sign-tool sign -selfSign 1(mandatory for OHOS runtime loading).How
Architecture
New
lib/libpng/ohos/namespace with MECE classes:OHOS::NDK— pure-data class for NDK path discovery. No shell calls except via explicit#download.OHOS::ZlibBuilder < MiniPortileCMake— static zlib build with the OHOS toolchain.OHOS::CodeSigner— wrapsbinary-sign-tool. Pure-data#sign_commandis testable without invoking the tool.OHOS::Recipe < Libpng::Recipe— orchestrates: ensure NDK → build zlib → configure libpng with toolchain + static zlib → build → sign.Libpng::Recipe.for_target(platform)factory (OCP seam): returnsOHOS::Recipefor*-ohos, baseRecipeotherwise. The existing recipe is unchanged except for this factory method.CI topology
Single
ubuntu-24.04-armjob (the OHOS NDK clang is x86_64 ELF). On the arm64 host we installqemu-user-static+binfmt-supportand register binfmt for x86_64 — NDK binaries (clang, binary-sign-tool) then run transparently via binfmt. dockerharmony verification runs natively because the arm64 rootfs matches the host arch.Why arm64 + binfmt instead of x86_64 + arm64-emulated docker:
Files added
lib/libpng/ohos.rb— module + autoloadslib/libpng/ohos/{ndk,zlib_builder,code_signer,recipe}.rbext/ohos/setup-ndk.sh— NDK download (adapted from ohos-node/build.sh)ext/ohos/smoke-test.c— minimal libpng round-trip (carried from PR OHOS: verify Alpine-built .so in dockerharmony (1.6.58.6) #12)ext/ohos/verify-prepare.sh— cross-compile smoke-test + bundle.so(also fixes PR OHOS: verify Alpine-built .so in dockerharmony (1.6.58.6) #12's symlink bug — now bundles all three SONAME forms)TODO.ohos/01-12-*.md— design documents for each pieceFiles modified
lib/libpng.rb— addautoload :OHOSlib/libpng/recipe.rb— addfor_targetfactory class methodext/extconf.rb— use factory.github/workflows/build.yml,release.yml— OHOS matrix entry moved out ofbuild_musl, dedicatedbuild_ohosjob addedlib/libpng/version.rb— iteration 5 → 6README.adoc,CHANGELOG.md,CLAUDE.mdWhat this verifies
aarch64-linux-ohos.libshared_libz.z.sodependency in the output).binary-sign-tool sign -selfSign 1succeeds on the resulting.so..soloads in real OHOS userland (dockerharmony) and round-trips a PNG encode/decode with matching bytes.What this does NOT verify
-selfSign 1produces a self-signed cert. Production Huawei-managed deployments may require re-signing with an enrolled cert.Test plan
bundle exec rake— 132 existing specs + 49 new OHOS specs pass (181 total)bundle exec rake rubocopcleanubuntu-24.04-armohos.toolchain.cmakebinary-sign-toolsigns the.soOKVersion
Bumps
LIBPNG_RUBY_ITERATION5 → 6. Replaces the regression acknowledged in CHANGELOG (1.6.58.4/.5 used Alpine-built bytes for OHOS without NDK).