Skip to content

Rename native extension and cross-compile via rb-sys-dock#23

Open
berkos wants to merge 12 commits into
fetlife:mainfrom
berkos:fix-release-workflow
Open

Rename native extension and cross-compile via rb-sys-dock#23
berkos wants to merge 12 commits into
fetlife:mainfrom
berkos:fix-release-workflow

Conversation

@berkos

@berkos berkos commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

PR #22 migrated the gem to rb-sys while preserving the historical libfacedetection_ruby native-library name. This PR deliberately aligns the gem, Cargo package, cdylib, extconf target, loader, and gem metadata on libfacedetection.

The upstream Rust crate is imported as libfacedetection_rs to avoid colliding with the local package name, while the public Cargo feature remains libfacedetection.

Extension layout

The layout follows the root-crate pattern introduced in distributing-iterator PR #8:

  • Keep Cargo.toml and src/lib.rs at the repository root.
  • Keep the install entrypoint at ext/libfacedetection/extconf.rb, loading a shared root extconf.rb.
  • Build the native extension as libfacedetection.
  • Package the root Cargo sources in the source gem.

Two fixes are included beyond the original PR #8 implementation:

  • Use a relative Cargo manifest path so source-gem installation does not generate a broken ./usr/local/.../Cargo.toml path.
  • Load ABI-specific native gems and RubyGems' extension_dir, because the original PR Rework API #8 loader cannot load its released ABI-versioned native artifact.

Release workflow

The 0.4.0 release failed because gem-compiler unpacked the source gem and ran extconf.rb without rb_sys on the load path (cannot load such file -- rb_sys/mkmf). Replace that path with Bundler + rb-sys-dock, matching distributing-iterator:

  • Build and attach the source gem.
  • Build native gems for x86_64-linux, aarch64-linux, x86_64-darwin, and arm64-darwin.
  • Publish all artifacts through the GitHub Release.
  • Keep OpenCV optional and disabled in precompiled gems.
  • Keep Windows excluded until the vendored C++ dependency is validated on MinGW.

Other changes

  • Use RbSys::ExtensionTask and Rake::TestTask.
  • Include Cargo.lock in the source gem.
  • Rename Readme.md to README.md so it is packaged on case-sensitive systems.
  • Run extension tests on Ruby 3.2, 3.3, 3.4, and 4.0 across Linux and macOS.
  • Replace deprecated Magnus global constructors/accessors with explicit &Ruby handle APIs.
  • Restore the Rust toolchain action's default -D warnings policy so future warnings fail CI.
  • Bump the gem version to 0.4.1.

Verification

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --locked -- -D warnings
  • RUSTFLAGS="-D warnings" bundle exec rake test on Ruby 3.4 and Ruby 4.0
  • bundle exec rake build
  • Installed the source gem in a clean Ruby 4.0 Linux container, compiled it, and loaded libfacedetection successfully.
  • bundle exec rb-sys-dock --platform x86_64-linux --build
  • Installed the generated Linux native gem in clean Ruby 3.4 and Ruby 4.0 containers and loaded libfacedetection successfully.
  • actionlint passes for both workflows.

berkos added 2 commits June 4, 2026 18:12
The native-gem cross-compile step used gem-compiler, which unpacks the
source gem and runs its extconf.rb in isolation — without rb_sys on the
load path. After the move to the rb-sys mkmf build, this fails with
`cannot load such file -- rb_sys/mkmf`.

Drop the precompiled-native-gem matrix and publish the source gem only.
The native extension is compiled at install time, so no functionality is
lost.
Supersedes the gem-compiler-based approach. gem-compiler unpacks the
source gem and runs extconf.rb in isolation, without rb_sys on the load
path; after the move to the rb-sys mkmf build, that step fails with
`cannot load such file -- rb_sys/mkmf`.

Replace it with the rake-compiler + rb-sys-dock pattern used by the
other in-house rb-sys gems (distributing_iterator,
gems/fetlife_markdown_renderer in fetlife-web):

- Rakefile: switch from Gem::PackageTask + gem-compiler to
  RbSys::ExtensionTask + a dock:build:* namespace that shells out to
  rb-sys-dock. Cross-platforms: x86_64-linux, aarch64-linux,
  x86_64-darwin, arm64-darwin.
- Gemfile: pin rake-compiler ~> 1.3 and rb_sys >= 0.9.126 for the
  cross-compile path.
- .github/workflows/release.yml: three-job pipeline mirroring
  distributing-iterator — build_source_gem, build_native_gems
  (matrix per platform via `bundle exec rb-sys-dock`), release
  (attach all gems to the tag).

The opencv cargo feature stays optional/off-by-default; cross-compiled
gems ship libfacedetection (vendored C++, built from source via cc-rs)
without opencv. Consumers who need opencv keep building from source.
@berkos berkos changed the title Fix release workflow Cross-compile native gems via rake-compiler + rb-sys-dock Jun 11, 2026
berkos added 8 commits June 11, 2026 12:49
The new Rakefile requires `bundler/setup` so it can use bundler-managed
deps (rake-compiler, rb_sys) for the cross-compile path. The old
run-tests workflow ran `rake gem` before `bundle install`, which now
aborts with `Bundler::GemNotFound`.

Modernize to match distributing-iterator/ci.yml:
- ruby/setup-ruby@v1 with bundler-cache: true (auto-runs bundle install)
- actions-rust-lang/setup-rust-toolchain@v1 for the native compile
- bundle exec rake build (replaces rake gem)
- Drop libopencv-dev — the default cargo feature set doesn't pull
  opencv, and the smoke test only uses ruby-vips
- Drop the long commented-out compile_native_gem block; the real
  cross-compile lives in release.yml now
RbSys::ExtensionTask.new("libfacedetection", ...) looks up cargo
package metadata by exact name match against `[package].name`. The
crate was named `libfacedetection-ruby` (chosen in the original rb-sys
migration to differentiate from the upstream `libfacedetection-rs` git
dep), so rake build aborts with `RbSys::PackageNotFoundError`.

Rename our package to match the gem name and alias the upstream dep
under a non-conflicting name:

- ext/libfacedetection/Cargo.toml:
  - [package].name: libfacedetection-ruby -> libfacedetection
  - [lib].name: libfacedetection_ruby -> libfacedetection
  - dep alias: libfacedetection_rs (package = "libfacedetection")
  - default feature: libfacedetection_rs (was libfacedetection)
- src/lib.rs: libfacedetection::facedetect_cnn -> libfacedetection_rs::,
  feature gates updated. Ruby-visible Symbol "libfacedetection" kept
  as the public feature label.
- libfacedetection.gemspec: cargo_crate_name metadata -> libfacedetection
- Cargo.lock: regenerated

extconf.rb / install-time bundle install path is unaffected — it just
runs `cargo build` against whatever is in the manifest.
actions-rust-lang/setup-rust-toolchain@v1 defaults to exporting
RUSTFLAGS=-D warnings, which turns the pre-existing magnus 0.8.2
deprecation warnings (Symbol::new, Integer::from_i64, RArray::new,
exception::runtime_error) into hard compile errors at `gem install`
time.

Opt out via `rustflags: ""`. The deprecations are real tech debt to
clean up later (every call site needs threading the Ruby handle
through), but not in scope for fixing the release workflow.

release.yml is unaffected: rb-sys-dock runs inside a docker container
with its own Rust toolchain, not the runner's env.
When cargo's [lib].name changed from libfacedetection_ruby to
libfacedetection, the generated Makefile target name didn't follow:
extconf.rb still asked rb_sys/mkmf to build "libfacedetection_ruby"
as the artifact, so make tried to copy a non-existent
target/release/liblibfacedetection_ruby.so and failed.

- ext/libfacedetection/extconf.rb: create_rust_makefile target ->
  "libfacedetection/libfacedetection"
- lib/libfacedetection.rb: load paths -> "libfacedetection" (no _ruby)

Mirrors gems/fetlife_markdown_renderer's loader pattern where gem
name = lib name = file name throughout.
`ruby tests/test_detection.rb` ran in system Ruby and couldn't find
ruby-vips, which is bundler-managed (vendor/bundle/, not system gems).
Switch to:

- `bundle exec rake compile` — builds the native extension into
  lib/libfacedetection/ in-place (loader picks it up via relative
  require).
- `bundle exec ruby tests/test_detection.rb` — bundler activates
  libfacedetection from the local gemspec source plus ruby-vips/ffi
  dev deps.

Add minitest to the Gemfile (test group) — the smoke test uses
minitest/autorun but it wasn't listed anywhere, so it only worked
when system Ruby happened to ship it as a bundled gem.
- Rakefile: drop the dock:build:* namespace + CROSS_PLATFORMS + the
  resolve_platforms helpers. Cross-compile is invoked directly via
  `bundle exec rb-sys-dock --platform X --build` from release.yml,
  so the rake tasks were dead weight. Add `Rake::TestTask` so
  `rake test` compiles via the `compile:dev` dep and runs the smoke
  test in one step. Drop the eager `require "bundler/setup"` —
  `bundler/gem_tasks` already pulls bundler in.
- Gemfile: revert to bare `source + gemspec`. All dev pins live in
  the gemspec (single-source-of-truth, matches distributing-iterator).
- libfacedetection.gemspec:
  - move minitest "~> 5.0" from Gemfile group :test
  - loosen rb_sys from "~> 0.9.126" to "~> 0.9" (matches
    distributing-iterator; lets future 0.9.x patches in)
- extconf.rb: add `config.profile = ENV.fetch("RB_SYS_CARGO_PROFILE",
  "release").to_sym` so debug builds are toggleable via env (QoL).
- workflow.yml: replace the explicit build/compile/test trio with a
  single `bundle exec rake test`.
@berkos berkos changed the title Cross-compile native gems via rake-compiler + rb-sys-dock Rename native extension and cross-compile via rb-sys-dock Jun 11, 2026
@berkos berkos requested a review from Antti June 11, 2026 13:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant