From 494a074821ee60b3a6b9ea3f76c2d4d2d8cec14d Mon Sep 17 00:00:00 2001 From: Da Young Kim Date: Thu, 2 Jul 2026 14:15:20 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20ruby=20=EB=B0=94=EC=9D=B8=EB=94=A9?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(magnus=20+=20rb-sys)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - packages/ruby: magnus 기반 네이티브 확장 (encode / translate_to_unicode / translate_to_braille_font) - 소스 gem은 crates.io braillify 사용, 저장소 빌드는 [patch.crates-io]로 로컬 코어 사용 - rb-sys-test-helpers 임베디드 VM 단위 테스트 + minitest 통합 테스트 - CI: ruby-test(3 OS) / ruby-build(cross-gem 5플랫폼) / ruby-publish(rubygems.org) 잡 추가 - changepacks에 packages/ruby/Cargo.toml 등록 (버전 단일 소스) Co-Authored-By: Claude Fable 5 --- .changepacks/config.json | 2 +- .github/workflows/publish.yml | 141 +++++++++++++++++++++ AGENTS.md | 1 + Cargo.lock | 169 ++++++++++++++++++++++++- Cargo.toml | 5 + packages/ruby/.gitignore | 7 + packages/ruby/Cargo.toml | 28 ++++ packages/ruby/Gemfile | 10 ++ packages/ruby/README.md | 33 +++++ packages/ruby/Rakefile | 22 ++++ packages/ruby/braillify.gemspec | 33 +++++ packages/ruby/build.rs | 6 + packages/ruby/extconf.rb | 8 ++ packages/ruby/lib/braillify.rb | 23 ++++ packages/ruby/lib/braillify/version.rb | 6 + packages/ruby/src/lib.rs | 104 +++++++++++++++ packages/ruby/test/braillify_test.rb | 41 ++++++ 17 files changed, 637 insertions(+), 2 deletions(-) create mode 100644 packages/ruby/.gitignore create mode 100644 packages/ruby/Cargo.toml create mode 100644 packages/ruby/Gemfile create mode 100644 packages/ruby/README.md create mode 100644 packages/ruby/Rakefile create mode 100644 packages/ruby/braillify.gemspec create mode 100644 packages/ruby/build.rs create mode 100644 packages/ruby/extconf.rb create mode 100644 packages/ruby/lib/braillify.rb create mode 100644 packages/ruby/lib/braillify/version.rb create mode 100644 packages/ruby/src/lib.rs create mode 100644 packages/ruby/test/braillify_test.rb diff --git a/.changepacks/config.json b/.changepacks/config.json index be707995..062c8198 100644 --- a/.changepacks/config.json +++ b/.changepacks/config.json @@ -1,5 +1,5 @@ { - "ignore": ["**", "!packages/python/pyproject.toml", "!packages/dotnet/BraillifyNet/BraillifyNet.csproj", "!packages/dotnet/Braillify/Braillify.csproj", "!packages/node/package.json", "!libs/braillify/Cargo.toml"], + "ignore": ["**", "!packages/python/pyproject.toml", "!packages/dotnet/BraillifyNet/BraillifyNet.csproj", "!packages/dotnet/Braillify/Braillify.csproj", "!packages/node/package.json", "!packages/ruby/Cargo.toml", "!libs/braillify/Cargo.toml"], "baseBranch": "main", "latestPackage": null, "publish": {} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bdb3a28d..88ac1b96 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -49,6 +49,11 @@ jobs: with: components: clippy, rustfmt - uses: actions-rust-lang/setup-rust-toolchain@v1 + # packages/ruby(rb-sys) 컴파일에 libruby가 필요하다. + # Windows는 MSVC Rust 툴체인과 링크가 맞는 mswin Ruby를 사용한다. + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.platform == 'windows-latest' && 'mswin' || '3.4' }} - name: Cargo tarpaulin and fmt run: | cargo install cargo-tarpaulin @@ -186,6 +191,42 @@ jobs: run: dotnet test --no-build -c Release --verbosity normal --framework net9.0 Braillify.Tests/Braillify.Tests.csproj working-directory: packages/dotnet + ruby-test: + name: Ruby Test - ${{ matrix.platform }} + runs-on: ${{ matrix.platform }} + strategy: + fail-fast: false + matrix: + platform: + - ubuntu-latest + - windows-latest + - macos-latest + steps: + # pull_request_target에서도 PR 코드를 체크아웃해 테스트한다. + - uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + + # Windows는 MSVC Rust 툴체인과 링크가 맞는 mswin Ruby를 사용한다. + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.platform == 'windows-latest' && 'mswin' || '3.4' }} + bundler-cache: true + working-directory: packages/ruby + + - name: Compile + run: bundle exec rake compile + working-directory: packages/ruby + + - name: Test + run: bundle exec rake test + working-directory: packages/ruby + deploy-landing: name: Deploy Landing runs-on: ubuntu-latest @@ -193,6 +234,7 @@ jobs: needs: - test - dotnet-test + - ruby-test steps: - uses: actions/checkout@v6 - uses: oven-sh/setup-bun@v2 @@ -239,6 +281,7 @@ jobs: needs: - test - dotnet-test + - ruby-test steps: - uses: actions/checkout@v6 - uses: changepacks/action@main @@ -798,3 +841,101 @@ jobs: with: name: nuget-packages path: packages/dotnet/**/nupkg/*.nupkg + + # ruby + ruby-build: + name: Ruby Build - ${{ matrix.platform }} + runs-on: ubuntu-latest + if: ${{ contains(needs.changepacks.outputs.changepacks, 'packages/ruby/Cargo.toml') }} + needs: + - test + - dotnet-test + - ruby-test + - changepacks + strategy: + fail-fast: false + matrix: + platform: + - x86_64-linux + - aarch64-linux + - x86_64-darwin + - arm64-darwin + - x64-mingw-ucrt + steps: + - uses: actions/checkout@v6 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.4" + bundler-cache: true + working-directory: packages/ruby + + # rb-sys-dock은 repo 루트를 마운트하므로 루트 워크스페이스의 + # [patch.crates-io]가 적용되어 로컬 libs/braillify로 빌드된다. + - uses: oxidize-rb/actions/cross-gem@v1 + id: cross-gem + with: + platform: ${{ matrix.platform }} + ruby-versions: "3.1,3.2,3.3,3.4" + working-directory: packages/ruby + + - name: Upload artifact + uses: actions/upload-artifact@v7 + with: + name: ruby-gem-${{ matrix.platform }} + path: ${{ steps.cross-gem.outputs.gem-path }} + if-no-files-found: error + + ruby-publish: + name: Ruby Publish + runs-on: ubuntu-latest + if: ${{ contains(needs.changepacks.outputs.changepacks, 'packages/ruby/Cargo.toml') }} + needs: + - changepacks + - ruby-build + permissions: + contents: write + id-token: write + steps: + - uses: actions/checkout@v6 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.4" + + - name: Download platform gems + uses: actions/download-artifact@v8 + with: + pattern: ruby-gem-* + path: gems + + # 소스 gem (Rust 툴체인으로 설치하는 fallback) + - name: Build source gem + run: | + gem build braillify.gemspec + mkdir -p ../../gems/source + mv braillify-*.gem ../../gems/source/ + working-directory: packages/ruby + + - name: Publish + env: + GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }} + run: | + VERSION=$(ruby -e 'print File.read("packages/ruby/Cargo.toml")[/^version = "([^"]+)"/, 1]') + + # 이미 publish된 버전/플랫폼 조합은 건너뛴다 (idempotent re-run). + # 최초 배포 시 versions 엔드포인트는 404를 반환하므로 빈 배열로 대체한다. + EXISTING=$(curl -fsSL "https://rubygems.org/api/v1/versions/braillify.json" || echo "[]") + + for GEM in $(find gems -name '*.gem' -type f); do + BASE=$(basename "$GEM" .gem) + PLATFORM=${BASE#braillify-$VERSION} + PLATFORM=${PLATFORM#-} + PLATFORM=${PLATFORM:-ruby} + if echo "$EXISTING" | jq -e --arg v "$VERSION" --arg p "$PLATFORM" \ + 'any(.[]; .number == $v and .platform == $p)' > /dev/null; then + echo "::notice::braillify-$VERSION ($PLATFORM) is already on rubygems.org. Skipping." + continue + fi + gem push "$GEM" + done diff --git a/AGENTS.md b/AGENTS.md index 1aa03bdf..fb0f75e1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,7 @@ - `libs/braillify/` — Rust 핵심 변환 엔진 - `packages/node/` — Node.js WASM 바인딩 - `packages/python/` — Python 바인딩 (maturin) +- `packages/ruby/` — Ruby 바인딩 (magnus + rb-sys, 워크스페이스 cargo 명령에 Ruby 3.x 필요) - `apps/landing/` — Next.js 랜딩 페이지 - `test_cases/` — 점자 변환 테스트 케이스 (JSON) - `docs/` — 2024 개정 한국 점자 규정 PDF diff --git a/Cargo.lock b/Cargo.lock index 9c862cde..8abc7d4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -144,6 +144,24 @@ dependencies = [ "windows-link", ] +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.2", + "shlex", + "syn", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -188,6 +206,17 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "braillify_rb" +version = "0.1.0" +dependencies = [ + "braillify", + "magnus", + "rb-sys", + "rb-sys-env", + "rb-sys-test-helpers", +] + [[package]] name = "bstr" version = "1.12.1" @@ -221,6 +250,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -260,6 +298,17 @@ dependencies = [ "half", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.6.1" @@ -412,7 +461,7 @@ dependencies = [ "lazy_static", "mintex", "parking_lot", - "rustc-hash", + "rustc-hash 1.1.0", "serde", "serde_json", "thousands", @@ -722,6 +771,16 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libm" version = "0.2.16" @@ -749,6 +808,29 @@ version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +[[package]] +name = "magnus" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b36a5b126bbe97eb0d02d07acfeb327036c6319fd816139a49824a83b7f9012" +dependencies = [ + "magnus-macros", + "rb-sys", + "rb-sys-env", + "seq-macro", +] + +[[package]] +name = "magnus-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47607461fd8e1513cb4f2076c197d8092d921a1ea75bd08af97398f593751892" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "memchr" version = "2.8.0" @@ -765,6 +847,12 @@ dependencies = [ "walkdir", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -811,6 +899,16 @@ dependencies = [ "wasm-bindgen-test", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -1225,6 +1323,57 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rb-sys" +version = "0.9.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ca28513560e56cfb79a62b1fce363c73af170a182024ce880c77ee9429920a" +dependencies = [ + "rb-sys-build", +] + +[[package]] +name = "rb-sys-build" +version = "0.9.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce04b2c55eff3a21aaa623fcc655d94373238e72cac6b3e1a3641ff31649f99a" +dependencies = [ + "bindgen", + "lazy_static", + "proc-macro2", + "quote", + "regex", + "shell-words", + "syn", +] + +[[package]] +name = "rb-sys-env" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca7ad6a7e21e72151d56fe2495a259b5670e204c3adac41ee7ef676ea08117a" + +[[package]] +name = "rb-sys-test-helpers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ccb543252549fc28f5d290322e041cd682bd199a8e1caaa813fb6e63dd221e" +dependencies = [ + "rb-sys", + "rb-sys-env", + "rb-sys-test-helpers-macros", +] + +[[package]] +name = "rb-sys-test-helpers-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1508caed999cb659ab1b3308e7b2985186b3b550ef5492dc18da71b560c55615" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1310,6 +1459,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1392,6 +1547,12 @@ version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +[[package]] +name = "seq-macro" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" + [[package]] name = "serde" version = "1.0.228" @@ -1435,6 +1596,12 @@ dependencies = [ "zmij", ] +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + [[package]] name = "shlex" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 78509576..14885d7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,11 @@ members = ["libs/*", "packages/*"] resolver = "3" +# packages/ruby는 소스 gem 배포를 위해 braillify를 crates.io 버전으로 의존한다. +# 저장소 내 빌드(개발/CI/프리컴파일 gem)는 이 patch로 로컬 코어를 사용한다. +[patch.crates-io] +braillify = { path = "libs/braillify" } + [workspace.package] version = "0.1.0" edition = "2024" diff --git a/packages/ruby/.gitignore b/packages/ruby/.gitignore new file mode 100644 index 00000000..90422341 --- /dev/null +++ b/packages/ruby/.gitignore @@ -0,0 +1,7 @@ +/tmp/ +/pkg/ +/.bundle/ +Gemfile.lock +/lib/braillify/**/*.so +/lib/braillify/**/*.bundle +/lib/braillify/**/*.dll diff --git a/packages/ruby/Cargo.toml b/packages/ruby/Cargo.toml new file mode 100644 index 00000000..17a34a47 --- /dev/null +++ b/packages/ruby/Cargo.toml @@ -0,0 +1,28 @@ +# NOTE: 이 crate는 소스 gem으로 배포되어 워크스페이스 밖에서 단독 빌드될 수 있으므로 +# workspace 상속(edition.workspace 등)을 사용하지 않는다. +# 저장소 내 빌드는 루트 Cargo.toml의 [patch.crates-io]로 로컬 libs/braillify를 사용한다. +# 패키지 이름은 rb_sys ExtensionTask가 cargo metadata에서 조회하는 키이므로 +# 확장 이름(braillify_rb)과 일치해야 한다. +[package] +name = "braillify_rb" +version = "0.1.0" +edition = "2024" +rust-version = "1.95" + +[lib] +name = "braillify_rb" +crate-type = ["cdylib", "rlib"] +doctest = false + +[dependencies] +magnus = "0.8" +# build.rs(rb-sys-env)가 DEP_RB_* 환경변수를 받으려면 rb-sys 직접 의존이 필요하다. +rb-sys = "0.9" +braillify = { version = "2", default-features = false } + +[build-dependencies] +rb-sys-env = "0.2" + +[dev-dependencies] +rb-sys = { version = "0.9", features = ["link-ruby"] } +rb-sys-test-helpers = "0.2" diff --git a/packages/ruby/Gemfile b/packages/ruby/Gemfile new file mode 100644 index 00000000..3df3f2e0 --- /dev/null +++ b/packages/ruby/Gemfile @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gemspec + +gem "minitest", "~> 5.25" +gem "rake", "~> 13.2" +gem "rake-compiler", "~> 1.2" +gem "rb_sys", "~> 0.9" diff --git a/packages/ruby/README.md b/packages/ruby/README.md new file mode 100644 index 00000000..0a631a2c --- /dev/null +++ b/packages/ruby/README.md @@ -0,0 +1,33 @@ +# braillify (Ruby) + +한국어 텍스트를 한국 점자(2024 개정 한국 점자 규정)로 변환하는 라이브러리의 Ruby 바인딩입니다. Rust 네이티브 확장(magnus + rb-sys)으로 동작합니다. + +## 설치 + +```bash +gem install braillify +``` + +주요 플랫폼(linux x86_64/arm64, macOS x86_64/arm64, Windows)용 프리컴파일 gem이 제공됩니다. 그 외 플랫폼은 소스 gem이 설치되며 Rust 툴체인이 필요합니다. + +## 사용법 + +```ruby +require "braillify" + +Braillify.translate_to_unicode("안녕하세요") # => 점자 유니코드 String +Braillify.translate_to_braille_font("안녕하세요") # => 점자 폰트 String +Braillify.encode("안녕하세요") # => ASCII-8BIT String (점자 셀 바이트) +``` + +변환할 수 없는 입력은 `ArgumentError`를 발생시킵니다. + +## 개발 + +```bash +bundle install +bundle exec rake compile # 네이티브 확장 빌드 +bundle exec rake test # minitest 실행 +``` + +Rust 쪽 단위 테스트는 저장소 루트에서 `cargo test -p braillify_rb`로 실행합니다 (Ruby 3.x 필요). diff --git a/packages/ruby/Rakefile b/packages/ruby/Rakefile new file mode 100644 index 00000000..f0bdc768 --- /dev/null +++ b/packages/ruby/Rakefile @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "rake/testtask" +require "rb_sys/extensiontask" + +GEMSPEC = Gem::Specification.load("braillify.gemspec") + +RbSys::ExtensionTask.new("braillify_rb", GEMSPEC) do |ext| + # 크레이트(Cargo.toml)가 표준 ext//이 아닌 패키지 루트에 있다 + # (루트 Cargo 워크스페이스의 members = ["packages/*"] 글롭 요구사항). + ext.ext_dir = "." + ext.lib_dir = "lib/braillify" + ext.cross_compile = true +end + +Rake::TestTask.new(:test) do |t| + t.libs << "test" << "lib" + t.test_files = FileList["test/**/*_test.rb"] +end + +task test: :compile +task default: :test diff --git a/packages/ruby/braillify.gemspec b/packages/ruby/braillify.gemspec new file mode 100644 index 00000000..351b1e3c --- /dev/null +++ b/packages/ruby/braillify.gemspec @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require_relative "lib/braillify/version" + +Gem::Specification.new do |spec| + spec.name = "braillify" + spec.version = Braillify::VERSION + spec.authors = ["JeongMin Oh"] + spec.email = ["owjs39@gmail.com"] + + spec.summary = "Rust 기반 크로스플랫폼 한국어 점역 라이브러리" + spec.description = "한국어 텍스트를 한국 점자(2024 개정 한국 점자 규정)로 변환하는 라이브러리. Rust 네이티브 확장으로 동작한다." + spec.homepage = "https://braillify.kr" + spec.license = "Apache-2.0" + spec.required_ruby_version = ">= 3.1" + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = "https://github.com/dev-five-git/braillify" + + spec.files = Dir[ + "lib/**/*.rb", + "src/**/*.rs", + "Cargo.toml", + "build.rs", + "extconf.rb", + "README.md" + ] + spec.require_paths = ["lib"] + spec.extensions = ["extconf.rb"] + + # 소스 gem 설치 시 extconf.rb가 rb_sys/mkmf를 require한다. + spec.add_dependency "rb_sys", "~> 0.9" +end diff --git a/packages/ruby/build.rs b/packages/ruby/build.rs new file mode 100644 index 00000000..60250623 --- /dev/null +++ b/packages/ruby/build.rs @@ -0,0 +1,6 @@ +fn main() -> Result<(), Box> { + // rb-sys-test-helpers 기반 `cargo test`가 임베디드 Ruby VM에 링크할 수 있도록 + // rbconfig에서 링크 플래그/cfg를 활성화한다. + let _ = rb_sys_env::activate()?; + Ok(()) +} diff --git a/packages/ruby/extconf.rb b/packages/ruby/extconf.rb new file mode 100644 index 00000000..9173dcc0 --- /dev/null +++ b/packages/ruby/extconf.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require "mkmf" +require "rb_sys/mkmf" + +# Cargo.toml은 이 파일과 같은 디렉토리에 있다. +# 산출물은 lib/braillify/braillify_rb.{so,bundle,dll}로 설치된다. +create_rust_makefile("braillify/braillify_rb") diff --git a/packages/ruby/lib/braillify.rb b/packages/ruby/lib/braillify.rb new file mode 100644 index 00000000..35235030 --- /dev/null +++ b/packages/ruby/lib/braillify.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require_relative "braillify/version" + +# 프리컴파일 gem은 Ruby 마이너 버전별 디렉토리(lib/braillify/3.4/...)에, +# 소스 빌드는 lib/braillify/ 바로 아래에 확장이 놓인다. +begin + ruby_minor = RUBY_VERSION[/\d+\.\d+/] + require_relative "braillify/#{ruby_minor}/braillify_rb" +rescue LoadError + require_relative "braillify/braillify_rb" +end + +# 한국어 텍스트를 한국 점자로 변환하는 모듈 (2024 개정 한국 점자 규정 기반). +# 메서드는 네이티브 확장(Rust)에서 정의된다: +# +# Braillify.encode(text) # => ASCII-8BIT String (점자 셀 바이트) +# Braillify.translate_to_unicode(text) # => 점자 유니코드 String +# Braillify.translate_to_braille_font(text) # => 점자 폰트 String +# +# 변환할 수 없는 입력은 ArgumentError를 발생시킨다. +module Braillify +end diff --git a/packages/ruby/lib/braillify/version.rb b/packages/ruby/lib/braillify/version.rb new file mode 100644 index 00000000..476d2784 --- /dev/null +++ b/packages/ruby/lib/braillify/version.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Braillify + # 버전 단일 소스는 Cargo.toml — changepacks가 bump하는 파일이다. + VERSION = File.read(File.expand_path("../../Cargo.toml", __dir__), encoding: "UTF-8")[/^version = "([^"]+)"/, 1].freeze +end diff --git a/packages/ruby/src/lib.rs b/packages/ruby/src/lib.rs new file mode 100644 index 00000000..3cc8113e --- /dev/null +++ b/packages/ruby/src/lib.rs @@ -0,0 +1,104 @@ +use magnus::{Error, RString, Ruby, function}; + +/// 텍스트를 점자 바이트(ASCII-8BIT String)로 인코딩합니다. +fn encode(ruby: &Ruby, text: String) -> Result { + braillify::encode(&text) + .map(|bytes| ruby.str_from_slice(&bytes)) + .map_err(|e| Error::new(ruby.exception_arg_error(), e)) +} + +/// 텍스트를 점자 유니코드 문자열로 인코딩합니다. +fn translate_to_unicode(ruby: &Ruby, text: String) -> Result { + braillify::encode_to_unicode(&text).map_err(|e| Error::new(ruby.exception_arg_error(), e)) +} + +/// 텍스트를 점자 폰트 문자열로 인코딩합니다. +fn translate_to_braille_font(ruby: &Ruby, text: String) -> Result { + braillify::encode_to_braille_font(&text).map_err(|e| Error::new(ruby.exception_arg_error(), e)) +} + +#[magnus::init] +fn init(ruby: &Ruby) -> Result<(), Error> { + let module = ruby.define_module("Braillify")?; + module.define_module_function("encode", function!(encode, 1))?; + module.define_module_function("translate_to_unicode", function!(translate_to_unicode, 1))?; + module.define_module_function( + "translate_to_braille_font", + function!(translate_to_braille_font, 1), + )?; + Ok(()) +} + +#[cfg(test)] +mod tests { + //! magnus 바인딩 테스트. + //! + //! `#[ruby_test]`(rb-sys-test-helpers)가 각 테스트 전에 임베디드 Ruby VM을 + //! 부트하므로 외부 Ruby 프로세스 없이 `Ruby::get()`이 사용 가능하다. + //! python 바인딩(packages/python/src/lib.rs)의 테스트 구성을 미러링한다. + use super::*; + use magnus::prelude::*; + use rb_sys_test_helpers::ruby_test; + + #[ruby_test] + fn encode_happy_path_returns_bytes() { + let ruby = Ruby::get().unwrap(); + let result = encode(&ruby, "안녕".to_string()).expect("encode must succeed"); + assert!(!result.is_empty()); + } + + #[ruby_test] + fn encode_engine_failure_maps_to_arg_error() { + let ruby = Ruby::get().unwrap(); + // 😀는 지원하지 않는 CharType → core encode가 Err 반환 → ArgumentError 매핑. + assert!(encode(&ruby, "😀".to_string()).is_err()); + } + + #[ruby_test] + fn translate_to_unicode_happy_path() { + let ruby = Ruby::get().unwrap(); + let result = translate_to_unicode(&ruby, "hi".to_string()).expect("must succeed"); + assert!(!result.is_empty()); + // 출력은 점자 유니코드(U+2800..=U+28FF) 범위여야 한다. + for ch in result.chars() { + let cp = ch as u32; + assert!((0x2800..=0x28FF).contains(&cp), "non-braille char {ch:?}"); + } + } + + #[ruby_test] + fn translate_to_unicode_failure_maps_to_arg_error() { + let ruby = Ruby::get().unwrap(); + assert!(translate_to_unicode(&ruby, "😀".to_string()).is_err()); + } + + #[ruby_test] + fn translate_to_braille_font_happy_path() { + let ruby = Ruby::get().unwrap(); + let result = translate_to_braille_font(&ruby, "hi".to_string()).expect("must succeed"); + assert!(!result.is_empty()); + } + + #[ruby_test] + fn translate_to_braille_font_failure_maps_to_arg_error() { + let ruby = Ruby::get().unwrap(); + assert!(translate_to_braille_font(&ruby, "😀".to_string()).is_err()); + } + + /// `#[magnus::init]` 등록 본문을 실행하고, 등록된 모듈 함수가 Ruby에서 + /// 실제로 호출 가능한지 funcall로 검증한다. + #[ruby_test] + fn init_registers_all_module_functions() { + let ruby = Ruby::get().unwrap(); + init(&ruby).expect("module init"); + let module = ruby.define_module("Braillify").unwrap(); + let unicode: String = module.funcall("translate_to_unicode", ("안녕",)).unwrap(); + assert!(!unicode.is_empty()); + let font: String = module + .funcall("translate_to_braille_font", ("안녕",)) + .unwrap(); + assert!(!font.is_empty()); + let bytes: RString = module.funcall("encode", ("안녕",)).unwrap(); + assert!(!bytes.is_empty()); + } +} diff --git a/packages/ruby/test/braillify_test.rb b/packages/ruby/test/braillify_test.rb new file mode 100644 index 00000000..6e9c548e --- /dev/null +++ b/packages/ruby/test/braillify_test.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require "minitest/autorun" +require "braillify" + +class BraillifyTest < Minitest::Test + def test_translate_to_unicode_returns_braille_unicode + out = Braillify.translate_to_unicode("안녕하세요") + refute_empty out + out.each_char do |c| + assert_includes 0x2800..0x28FF, c.ord, "non-braille char: #{c.inspect}" + end + end + + def test_encode_returns_binary_string + out = Braillify.encode("안녕") + assert_equal Encoding::ASCII_8BIT, out.encoding + refute_empty out + end + + def test_encode_bytes_match_unicode_codepoints + text = "안녕하세요" + unicode = Braillify.translate_to_unicode(text) + bytes = Braillify.encode(text) + assert_equal unicode.chars.map { |c| c.ord - 0x2800 }, bytes.bytes + end + + def test_translate_to_braille_font + refute_empty Braillify.translate_to_braille_font("hi") + end + + def test_unsupported_input_raises_argument_error + assert_raises(ArgumentError) { Braillify.translate_to_unicode("😀") } + assert_raises(ArgumentError) { Braillify.encode("😀") } + assert_raises(ArgumentError) { Braillify.translate_to_braille_font("😀") } + end + + def test_version_matches_cargo_toml + assert_match(/\A\d+\.\d+\.\d+\z/, Braillify::VERSION) + end +end From 11eb9c87793e4ba54553726f5be579d13cc7f528 Mon Sep 17 00:00:00 2001 From: Da Young Kim Date: Thu, 2 Jul 2026 14:44:09 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20ruby=20CI=EB=A5=BC=20setup-ruby-and-?= =?UTF-8?q?rust=20=EC=95=A1=EC=85=98=EC=9C=BC=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=ED=99=98,=20Windows=20=EA=B0=9C=EB=B0=9C=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Windows 로컬 검증(ucrt Ruby 3.4 + GNU Rust)에서 cargo test/rake compile/minitest 전부 통과 확인 - mswin(개발 빌드) Ruby 대신 rb-sys 공식 setup-ruby-and-rust 액션으로 툴체인 매칭 위임 - Windows bindgen(libclang) 환경변수 요구사항 README에 문서화 Co-Authored-By: Claude Fable 5 --- .github/workflows/publish.yml | 21 ++++++++++----------- packages/ruby/README.md | 11 +++++++++++ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 88ac1b96..4e825b1a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -50,10 +50,12 @@ jobs: components: clippy, rustfmt - uses: actions-rust-lang/setup-rust-toolchain@v1 # packages/ruby(rb-sys) 컴파일에 libruby가 필요하다. - # Windows는 MSVC Rust 툴체인과 링크가 맞는 mswin Ruby를 사용한다. - - uses: ruby/setup-ruby@v1 + # setup-ruby-and-rust가 Ruby 플랫폼(Windows ucrt 등)에 맞는 + # Rust 타겟/bindgen 환경을 함께 구성한다. + - uses: oxidize-rb/actions/setup-ruby-and-rust@v1 with: - ruby-version: ${{ matrix.platform == 'windows-latest' && 'mswin' || '3.4' }} + ruby-version: "3.4" + cargo-cache: true - name: Cargo tarpaulin and fmt run: | cargo install cargo-tarpaulin @@ -207,16 +209,13 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha || github.sha }} - - name: Setup Rust - uses: actions-rust-lang/setup-rust-toolchain@v1 + # Ruby 플랫폼(Windows ucrt 등)에 맞는 Rust 타겟/bindgen 환경까지 구성하는 + # rb-sys 공식 셋업 액션. + - uses: oxidize-rb/actions/setup-ruby-and-rust@v1 with: - toolchain: stable - - # Windows는 MSVC Rust 툴체인과 링크가 맞는 mswin Ruby를 사용한다. - - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ matrix.platform == 'windows-latest' && 'mswin' || '3.4' }} + ruby-version: "3.4" bundler-cache: true + cargo-cache: true working-directory: packages/ruby - name: Compile diff --git a/packages/ruby/README.md b/packages/ruby/README.md index 0a631a2c..0965c7bd 100644 --- a/packages/ruby/README.md +++ b/packages/ruby/README.md @@ -31,3 +31,14 @@ bundle exec rake test # minitest 실행 ``` Rust 쪽 단위 테스트는 저장소 루트에서 `cargo test -p braillify_rb`로 실행합니다 (Ruby 3.x 필요). + +### Windows 개발 환경 + +RubyInstaller(ucrt) + DevKit(MSYS2) + **GNU Rust 툴체인**(`x86_64-pc-windows-gnu`) 조합을 사용합니다. bindgen이 libclang을 요구하므로 LLVM 설치 후 아래 환경변수가 필요합니다 (``은 LLVM 메이저 버전): + +```powershell +$env:LIBCLANG_PATH = "C:\Program Files\LLVM\bin" +$env:BINDGEN_EXTRA_CLANG_ARGS = '--target=x86_64-w64-mingw32 -I"C:/Program Files/LLVM/lib/clang//include" -I/msys64/ucrt64/include' +``` + +`rake compile`은 `ridk exec` (MSYS2 환경) 안에서 실행합니다. From ee2aa5b986351c9c44ae79ed9cbfb922e9f56797 Mon Sep 17 00:00:00 2001 From: Da Young Kim Date: Thu, 2 Jul 2026 15:59:58 +0900 Subject: [PATCH 3/3] =?UTF-8?q?ci:=20Windows=20Ruby=20MSVC=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/publish.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1d7f23c1..9c939508 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -50,11 +50,12 @@ jobs: components: clippy, rustfmt - uses: actions-rust-lang/setup-rust-toolchain@v1 # packages/ruby(rb-sys) 컴파일에 libruby가 필요하다. - # setup-ruby-and-rust가 Ruby 플랫폼(Windows ucrt 등)에 맞는 - # Rust 타겟/bindgen 환경을 함께 구성한다. + # Windows는 pyo3/maturin(MSVC Python)과 ABI를 맞추기 위해 MSVC 빌드(mswin) Ruby를 쓴다. + # (ucrt Ruby는 GNU 툴체인을 요구해 한 번의 workspace-wide cargo 호출에서 pyo3와 공존 불가) + # mswin은 ruby master dev 빌드만 제공된다 — 실배포 플랫폼(ucrt) 검증은 ruby-test 잡이 담당. - uses: oxidize-rb/actions/setup-ruby-and-rust@v1 with: - ruby-version: "3.4" + ruby-version: ${{ matrix.platform == 'windows-latest' && 'mswin' || '3.4' }} cargo-cache: true - name: Cargo tarpaulin and fmt run: | @@ -218,6 +219,11 @@ jobs: cargo-cache: true working-directory: packages/ruby + # 실배포 gem 플랫폼(stable Ruby, Windows는 ucrt+GNU)에서의 lint. + # 루트 워크스페이스에서 실행해 [patch.crates-io]로 로컬 libs/braillify를 사용한다. + - name: Lint + run: cargo clippy -p braillify_rb --all-targets -- -D warnings + - name: Compile run: bundle exec rake compile working-directory: packages/ruby