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/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 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/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 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