From 6ac5e104feaed20d0c8c7de9419146c8d479dd44 Mon Sep 17 00:00:00 2001 From: Chris Petersen Date: Mon, 7 Sep 2026 15:04:16 -0700 Subject: [PATCH 1/3] Support Ruby 4.0 (magnus 0.8) and ship a 4.0 binary spellkit could not be installed on Ruby 4.0 at all. The precompiled platform gem carries one binary per ABI for 3.1-3.4 only, so RubyGems fell back to the source gem, and that failed to compile: magnus 0.7 does not support Ruby 4.0's C API. Two errors, both inside magnus rather than this crate - rb_fiber_raise's argv became *mut VALUE, and rb_sys::RTypedData lost its typed_flag field. Found while installing this gem on a clean GitHub runner, where ruby-version: ruby resolves to 4.0.x. magnus 0.8 fixes both, matching red-candle, which is already on 0.8. Verified by compiling and running the full suite on 3.3.0, 3.4.8, 4.0.1 and 4.0.6: all four compile, all four report the same 186 examples / 2 failures. Those 2 are pre-existing and version-independent - both hit DEFAULT_DICTIONARY_URL and fail under WebMock without a warm cache, identically on 3.4.8. ruby-versions is set explicitly to "3.1,3.2,3.3,3.4,4.0" in both workflows. The shared rust-gem-release default excludes 4.0 on purpose and its own docs name the exact failure above, so adding it is the documented opt-in for a gem whose native deps can handle it. benchmark is now a development dependency: it left the default gems in Ruby 4.0, so spec/performance_spec.rb's require failed there before any spec could run. Also clears every magnus deprecation (define_module, class::object, RArray::new, RHash::new -> their Ruby:: equivalents), taking the build from 7 warnings to 0. init already received a &Ruby it was ignoring; stats now takes one the same way suggestions and correct_tokens already did. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CiDy8MvBncxzGnnmvDJMtL --- .github/workflows/build.yml | 3 +++ .github/workflows/release.yml | 5 +++++ ext/spellkit/Cargo.toml | 2 +- ext/spellkit/src/lib.rs | 17 +++++++++-------- lib/spellkit/version.rb | 2 +- spellkit.gemspec | 3 +++ 6 files changed, 22 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index de4b642..0a890bf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,3 +12,6 @@ jobs: uses: scientist-labs/rust-gem-release/.github/workflows/build.yml@0.11.0 with: test-command: bundle exec rake spec + # Match the fat-gem ABI set in release.yml, so PR CI compiles against every + # Ruby we ship a binary for - including 4.0. + ruby-versions: "3.1,3.2,3.3,3.4,4.0" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e7339db..766373e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,6 +57,11 @@ jobs: # "spellkit", so ext-name / gemspec / platform-gem-env all resolve to their # defaults (spellkit, spellkit.gemspec, RUST_GEM_PLATFORM) and are omitted. gem-name: spellkit + # 4.0 added in 0.3.0. The shared workflow's default is 3.1-3.4 and deliberately + # excludes 4.0, because Ruby 4.0 changed the C API (rb_sys::RTypedData lost + # typed_flag, rb_fiber_raise's argv became *mut) and magnus 0.7 cannot compile + # against it. spellkit is on magnus 0.8 as of this version, so it can. + ruby-versions: "3.1,3.2,3.3,3.4,4.0" version-command: ruby -r./lib/spellkit/version -e 'print SpellKit::VERSION' version: ${{ inputs.version }} bump-command: | diff --git a/ext/spellkit/Cargo.toml b/ext/spellkit/Cargo.toml index b8030d5..667e713 100644 --- a/ext/spellkit/Cargo.toml +++ b/ext/spellkit/Cargo.toml @@ -11,7 +11,7 @@ name = "spellkit" crate-type = ["cdylib"] [dependencies] -magnus = { version = "0.7", features = ["rb-sys"] } +magnus = { version = "0.8", features = ["rb-sys"] } hashbrown = "0.15" unicode-normalization = "0.1" regex = "1.11" diff --git a/ext/spellkit/src/lib.rs b/ext/spellkit/src/lib.rs index 14b22f6..8b1017c 100644 --- a/ext/spellkit/src/lib.rs +++ b/ext/spellkit/src/lib.rs @@ -1,7 +1,7 @@ mod symspell; mod guards; -use magnus::{class, define_module, function, method, prelude::*, Error, RArray, RHash, Ruby, Value, TryConvert}; +use magnus::{function, method, prelude::*, Error, RArray, RHash, Ruby, Value, TryConvert}; use std::sync::{Arc, RwLock}; use symspell::SymSpell; use guards::Guards; @@ -264,10 +264,10 @@ impl Checker { if let Some(ref symspell) = state.symspell { let suggestions = symspell.suggestions(&word, max_suggestions); - let result = RArray::new(); + let result = ruby.ary_new(); for suggestion in suggestions { - let hash = RHash::new(); + let hash = ruby.hash_new(); hash.aset("term", suggestion.term)?; hash.aset("distance", suggestion.distance)?; hash.aset("freq", suggestion.frequency)?; @@ -320,7 +320,7 @@ impl Checker { return Err(Error::new(ruby.exception_runtime_error(), "Dictionary not loaded. Call load! first")); } - let result = RArray::new(); + let result = ruby.ary_new(); if let Some(ref symspell) = state.symspell { for token in tokens.into_iter() { @@ -336,8 +336,9 @@ impl Checker { } fn stats(&self) -> Result { + let ruby = Ruby::get().unwrap(); let state = self.state.read().unwrap(); - let stats = RHash::new(); + let stats = ruby.hash_new(); if !state.loaded { stats.aset("loaded", false)?; @@ -376,9 +377,9 @@ impl Checker { } #[magnus::init] -fn init(_ruby: &Ruby) -> Result<(), Error> { - let module = define_module("SpellKit")?; - let checker_class = module.define_class("Checker", class::object())?; +fn init(ruby: &Ruby) -> Result<(), Error> { + let module = ruby.define_module("SpellKit")?; + let checker_class = module.define_class("Checker", ruby.class_object())?; checker_class.define_singleton_method("new", function!(Checker::new, 0))?; checker_class.define_method("load!", method!(Checker::load_full, 1))?; diff --git a/lib/spellkit/version.rb b/lib/spellkit/version.rb index 23d9915..3ace8a6 100644 --- a/lib/spellkit/version.rb +++ b/lib/spellkit/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SpellKit - VERSION = "0.2.0" + VERSION = "0.3.0" end diff --git a/spellkit.gemspec b/spellkit.gemspec index 2240e20..afd66bd 100644 --- a/spellkit.gemspec +++ b/spellkit.gemspec @@ -56,6 +56,9 @@ Gem::Specification.new do |spec| spec.add_development_dependency "webmock" spec.add_development_dependency "standard", "~> 1.3" spec.add_development_dependency "irb" + # benchmark left the default gems in Ruby 4.0, so spec/performance_spec.rb's + # `require "benchmark"` fails there unless it is declared. + spec.add_development_dependency "benchmark" spec.add_development_dependency "benchmark-ips" spec.add_development_dependency "ffi-aspell" end From 6b5334d1e6d17b22ef23800edb759b529ad87e98 Mon Sep 17 00:00:00 2001 From: Chris Petersen Date: Mon, 7 Sep 2026 15:04:39 -0700 Subject: [PATCH 2/3] Correct the platform-support list It claimed Ruby 3.1-3.3, which was already stale before this release: 3.4 has shipped in the fat gem for some time, and 4.0 lands in 0.3.0. Also corrects the platform list to what the release workflow actually builds (arm64 darwin; x86_64 and aarch64 linux) rather than the macOS-x86_64/musl set it claimed, and says plainly what happens outside that list - a source build needing Rust. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CiDy8MvBncxzGnnmvDJMtL --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 29a41a6..ab223ca 100644 --- a/README.md +++ b/README.md @@ -684,9 +684,13 @@ bundle exec rake build ## Platform Support Pre-built gems available for: -- macOS (x86_64, arm64) -- Linux (glibc, musl) -- Ruby 3.1, 3.2, 3.3 +- macOS (arm64) +- Linux (x86_64, aarch64) +- Ruby 3.1, 3.2, 3.3, 3.4, 4.0 + +A pre-built gem carries one compiled extension per Ruby ABI, so installing under a Ruby +outside that list falls back to building from source and needs a Rust toolchain (>= 1.85). +Ruby 4.0 support arrived in 0.3.0; earlier versions cannot compile against it at all. ## Contributing From 5c8678f7f1ab0d0bbbe99ed4a4a4aa0d86033f43 Mon Sep 17 00:00:00 2001 From: Chris Petersen Date: Mon, 7 Sep 2026 15:08:16 -0700 Subject: [PATCH 3/3] Let :integration specs reach the network, unbreaking CI Every build run on this repo has been failing, on every branch and every Ruby - dependabot, ci/version-box-dispatch and migrate-to-rust-gem-release are all red for the same reason, so this predates the Ruby 4.0 work and blocks any clean release. Cause: dictionary_loading_spec.rb does `require "webmock/rspec"`, which disables net connect for the WHOLE suite rather than that one file. The two examples tagged :integration exist precisely to prove DEFAULT_DICTIONARY_URL really resolves and loads, so WebMock blocked them and they failed with NetConnectNotAllowedError. Collateral damage from a require in an unrelated file. An around hook now lets a :integration-tagged example reach the network and restores the block afterwards, so nothing else inherits it. 186 examples, 0 failures on 3.3.0, 3.4.8, 4.0.1 and 4.0.6 - recompiling between runs, since rake compile overwrites the flat lib/spellkit/spellkit.bundle and a stale one from another ABI fails ~170 examples in a way that looks alarming and is not real. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CiDy8MvBncxzGnnmvDJMtL --- spec/spec_helper.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 844294a..0c3b75a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -17,4 +17,17 @@ config.expect_with :rspec do |c| c.syntax = :expect end + + # `require "webmock/rspec"` in dictionary_loading_spec.rb disables net connect for the + # WHOLE suite, not just that file - so the two examples tagged :integration, whose entire + # purpose is to prove DEFAULT_DICTIONARY_URL really resolves and loads, were blocked by + # WebMock and failed on every Ruby. That is why every build run on this repo has been red. + # + # Let a tagged example reach the network, and restore the block afterwards so no other + # example inherits it. + config.around(:each, :integration) do |example| + WebMock.allow_net_connect! if defined?(WebMock) + example.run + WebMock.disable_net_connect!(allow_localhost: true) if defined?(WebMock) + end end