ruby/ruby.wasm を使って Redmine をブラウザ内で動かす話


My Redmine

前田 稔です。プログラミング言語Rubyの国際会議 RubyKaigi 2024 のセッション「RubyGems on ruby.wasm」で紹介された Mastodon in the browser demo を拝見し、 Redmine も同様にブラウザ内で動作させられるのではないかと考えました。本記事では、その検証結果と Wasm 化の手順についてまとめています。

期待と検証結果

Mastodon は Ruby on Rails で開発されたアプリケーションであり、Redmine も同じく Ruby on Rails で構築されていることから、ブラウザ内での動作が可能ではないかと期待しました。

結果として、Redmine をブラウザ内で動作させることに成功しました。しかし、ディスクへのアクセスが必要な機能(例:添付ファイルのアップロードなど)は動作しませんでした。この問題は、クラウドストレージを利用するプラグインを導入することで解決できる可能性があります。

なお、今回の検証では開発版のソースコードを使用しているため、将来的なアップデートにより本記事の手順では動作しなくなる可能性があります。

ブラウザ内で Redmine が動く様子
ブラウザ内で Redmine が動く様子

Redmine を Wasm 化する手順

今回の検証では、 Yuta Saito 氏(@kateinoigakukun) が公開している Mastodon in the browser demo のソースコードを参考にしました。

palkan/wasmify-rails: Tools and extensions to pack and run Rails apps on Wasm という他の選択肢もありましたが、今回は未検証です。

なお、 Wasm 化された Ruby on Rails アプリケーションがブラウザ内でどのように動作するかについては、 [SF Ruby, March 2024] Rails on Wasm - Speaker Deck をご覧ください( 動画 )。

検証環境

今回の検証には、ruby/ruby.wasm リポジトリ内の Dockerfile を基に環境を構築しました。使用した主要なツールのバージョンは以下の通りです。

$ ruby -v
ruby 3.4.0preview1 (2024-05-16 master 9d69619623) [x86_64-linux]

$ cargo --version
cargo 1.79.0 (ffa9cf99a 2024-06-03)

$ node --version
v20.17.0

$ cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

$ printenv | grep WASI
WASI_SDK_PATH=/usr/local/wasi-sdk-21.0

環境構築の詳細については、パッケージ追加に関するドキュメント [1] [2] [3] を参照してください。

1. WASI SDK の取得が必要です。詳細は https://github.com/WebAssembly/wasi-sdk?tab=readme-ov-file#installhttps://github.com/kateinoigakukun/wasi-vfs/blob/v0.5.3/.github/workflows/main.yml#L30-L39 を参照してください。

2. libidn のビルドに必要なパッケージの追加が必要です。詳細は tmp/libidn/CONTRIBUTING.md や https://www.gnu.org/software/libidn/#howtouseit ならびに https://github.com/coreutils/gnulib/blob/master/DEPENDENCIES を参照してください。

3. Ruby のビルドに必要なパッケージの追加が必要です。詳細は https://github.com/rbenv/ruby-build/wiki を参照してください。

RubyGems を含めた Redmine の Wasm 化

今回の検証では、 Mastodon in the browser demo のソースコードを参考にしました。その中でも、特に重要な役割を果たしたディレクトリやファイルを以下に示します。

以下に、私が理解した内容をまとめます。

bin/rbwasm

Wasm 化の処理が記載されています。大まかには以下の処理を行っています。

  1. rbconfig.rb を生成する
  2. --target-rbconfig に対応した vendor/rubygems を使用して web グループの RubyGems を bundle install する
  3. Wasm 化 + アプリケーションをパックする [4]

今回の検証では、不要な部分 [5] を削除するだけで問題ありませんでした。

4. https://speakerdeck.com/kateinoigakukun/what-you-can-do-with-ruby-on-webassemblyhttps://evilmartians.com/events/assembling-the-future-ruby-on-wasm-puzzle-euruko のスライドがわかりやすいです。

5. Mastodon 向けの .env.production や app.json の配置を指しています。

vendor/rubygems

--target-rbconfig に対応した kateinoigakukun/rubygems です [6]

6. 本来は --target-rbconfig に対応した https://github.com/ruby/ruby/tree/7cbe54714ca1b9112e278d2d605cd049a065707e を使用したかったのですが、 default gems の更新を把握できておらず、断念しました。

build_manifest.json

bundle exec rbwasm build --ruby-version head で使用する Ruby のリビジョンを指定します。

Mastodon in the browser demo の ruby/ruby.wasm で使用されている Ruby のリビジョンは 61829eec657e8271f05a74f2067b4203ba0a51a2 です。

今回の検証では 61829eec657e8271f05a74f2067b4203ba0a51a2 に近く、導入しやすい Ruby 3.4.0 preview1 に相当する 9d69619623ec6b86c464b7cac911b7201f74dab7 を使用しています。

patches/gnulib/

gnulib のパッチです。

pwa/

dist/pglite.rbdist/rails.main.rb など サービスワーカー でやり取りするデータベースサーバーやアプリケーションサーバー相当のものが含まれています。今回の検証では、 dist/rails.main.rb の不要な部分 [7] を削除するだけで問題ありませんでした。

また package.json については以下のバージョンに変更しています [8]

パッケージ バージョン
@bytecodealliance/jco 1.7.0
@bytecodealliance/preview2-shim 0.17.0

7. Mastodon 向けの class Status を指しています。

8. src/rails.sw.js に wasi:http/outgoing-handler と wasi:http/types の追記が必要です。

Gemfile

bin/rbwasm に必要な RubyGems および dist/pglite.rbdist/rails.main.rb で読み込まれる RubyGems が記載されています。

ruby/ruby.wasm について

Mastodon in the browser demo で使用されている ruby/ruby.wasm は https://github.com/ruby/ruby.wasm/tree/576cc3f6d838b0f942e045dd88fb295347d2291c です。

今回の検証も同じものを使用します [9] 。ただし、開発版の ruby/ruby.wasm で対応されている Use compiled API as fallback for rb-sys | ruby/ruby.wasm@1576fd0 を反映した状態にします。

また CONTRIBUTING.mdInstall dependencies に従って ruby_wasm.so を生成します。

9. 本来は最新の ruby/ruby.wasm を使用したかったのですが、更新内容を十分に把握できなかったため、断念しました。

Redmine の Gemfile について

開発版の 5.1.3.devel を使用し、 require が必要な RubyGems を web グループに指定します。

また、予期せぬエラーが発生しないように Ruby on Rails のダウングレードや default gems のバージョンを固定します。なお、 Cross-compile C-Extentions が必要な RubyGems は個別に Yuta Saito 氏(@kateinoigakukun) が公開されている対応済みのものを指定します。

変更後の Gemfile(変更箇所抜粋):

source 'https://rubygems.org'

ruby '>= 3.1.0', '< 3.4.0'

gem 'rails', '7.1.4', group: [:default, :web]
gem 'rouge', '~> 4.2', group: [:default, :web]
gem 'mini_mime', '~> 1.1.0'
gem "actionpack-xml_parser"
gem 'roadie-rails', '~> 3.2.0', group: [:default, :web]
gem 'marcel'
gem 'mail', '~> 2.8.1'
gem 'nokogiri', github: 'kateinoigakukun/nokogiri', ref: '8e9904e5a891af43ad0c1e8eec467ecbbf55d55f', group: [:default, :web]
gem 'i18n', '~> 1.14.1'
gem 'rbpdf', '~> 1.21.3', group: [:default, :web]
gem 'addressable'
gem 'rubyzip', '~> 2.3.0', group: [:default, :web]
gem 'propshaft', '~> 0.8.0', group: [:default, :web]
gem 'rack', '>= 3.1.3'

#  Ruby Standard Gems
gem 'csv', '~> 3.2.8', group: [:default, :web]
gem 'net-imap', '~> 0.4.8'
gem 'net-pop', '~> 0.1.2'
gem 'net-smtp', '~> 0.4.0'

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', group: [:default, :web]

# TOTP-based 2-factor authentication
gem 'rotp', '>= 5.0.0'
gem 'rqrcode'

# HTML pipeline and sanitization
gem "html-pipeline", "~> 2.13.2", group: [:default, :web]
gem "sanitize", "~> 6.0", group: [:default, :web]

# Optional gem for LDAP authentication
group :ldap do
  gem 'net-ldap', '~> 0.17.0'
end

# Optional gem for exporting the gantt to a PNG file
group :minimagick do
  gem 'mini_magick', '~> 4.13.0'
end

# Optional Markdown support
group :markdown do
  gem 'redcarpet', '~> 3.6.0', group: [:default, :web]
end

# Optional CommonMark support, not for JRuby
group :common_mark do
  gem "commonmarker", '~> 0.23.8'
  gem 'deckar01-task_list', '2.3.2', group: [:default, :web]
end

# 中略 ...

# Load plugins' Gemfiles
Dir.glob File.expand_path("../plugins/*/{Gemfile,PluginGemfile}", __FILE__) do |file|
  eval_gemfile file
end

# 以下、追加
gem 'psych', '5.1.2'
gem 'stringio', '3.1.1'
gem 'io-console', '0.7.2'
# gem 'io-console', github: 'ruby/io-console', ref: 'ba9bf00184ea7d5fdfb72945c8f42458bafc42aa'
gem 'nio4r', github: 'kateinoigakukun/nio4r', ref: 'd219d9bce40435bd993b6ed6e425ae7e76b62d04'

group :development do
  install_if -> { !(RUBY_PLATFORM =~ /wasm/) } do
    gem 'ruby_wasm', path: "vendor/ruby.wasm"
  end
end

gem 'activerecord-nulldb-adapter', group: [:web]
gem 'js', git: 'https://github.com/ruby/ruby.wasm',
  ref: "0ca30636702eb7e1bb2a17b3868a458a03f045a8", # branch: katei/kaigi-staging
  glob: 'packages/gems/js/*.gemspec', group: [:web]

config/database.yml

ENV["RAILS_WEB"] の値を評価して pglite を使用する仕組みに変更します。

default: &default
  adapter: postgresql
  encoding: unicode
  host: localhost
  username: postgres

development:
  <<: *default
  database: redmine_development

production:
  <<: *default
  database: redmine_production

Redmine の変更箇所

Redmine の Wasm 化に伴い、機能しない箇所やエラーが発生する箇所に対して修正を加えました。以下に主な修正箇所を示します。

app/models/auth_source_ldap.rb(変更箇所抜粋):

+if ENV["RAILS_WEB"]
+  class AuthSourceLdap < AuthSource; end
+  return
+end
+
 require 'net/ldap'
 require 'net/ldap/dn'
 require 'timeout'

config/application.rb(変更箇所抜粋):

     # Verify validity of user sessions
-    config.redmine_verify_sessions = true
+    config.redmine_verify_sessions = ENV["RAILS_WEB"].blank?

lib/redmine/wiki_formatting/html_sanitizer.rb(変更箇所抜粋):

   module WikiFormatting
     # Combination of SanitizationFilter and ExternalLinksFilter
     class HtmlSanitizer
-      Pipeline = HTML::Pipeline.new(
+      Pipeline = ::HTML::Pipeline.new(

デモデータ

以下のコマンドで作成したテキスト形式ダンプファイル [10]pwa/dist/redmine_development.sql を配置し、 pwa/src/rails.sw.jsmastodon_development.sql から変更します。

10. SET コマンド行のコメントアウトが必要です。

pg_dump -U postgres -h localhost --format=plain --column-inserts --file=/tmp/redmine_development.sql -d redmine_development

Wasm 化と起動手順

./bin/rbwasm を実行すると、以下のような出力が得られます [11]

11. 処理時間は、CPU 性能とディスク I/O に依存します。例えば、Docker を使用しない 8 コア/ 16 スレッドの Debian 環境では、処理が完了するまでにおおよそ20分程度かかります。一方、 Docker のボリューム機能を使用する macOS や Windows 環境では、30分以上かかります(Docker 上でボリューム機能を使用しない場合、実行時にエラーが発生する可能性があります)。

# 中略

./pwa/node_modules/@bytecodealliance/jco/src/jco.js transpile --instantiation --valid-lifting-optimization tmp/ruby.wasm -o pwa/dist/component

  Transpiled JS Component Files:

 - pwa/dist/component/interfaces/ruby-js-js-runtime.d.ts           1.89 KiB
 - pwa/dist/component/interfaces/ruby-js-ruby-runtime.d.ts         1.17 KiB
 - pwa/dist/component/interfaces/wasi-cli-environment.d.ts         0.15 KiB
(中略 ...)
 - pwa/dist/component/interfaces/wasi-random-random.d.ts           0.09 KiB
 - pwa/dist/component/ruby.core.wasm                               74.6 KiB
 - pwa/dist/component/ruby.core10.wasm                             35.3 KiB
 - pwa/dist/component/ruby.core11.wasm                             2.36 KiB
(中略 ...)
 - pwa/dist/component/ruby.core39.wasm                              482 KiB
 - pwa/dist/component/ruby.core4.wasm                              20.5 MiB
 - pwa/dist/component/ruby.core40.wasm                               66 KiB
(中略 ...)
 - pwa/dist/component/ruby.d.ts                                    4.04 KiB
 - pwa/dist/component/ruby.js                                       578 KiB

node ./build.mjs

  dist/rails.sw.js      831.4kb
  dist/boot.js            2.2kb
  dist/rails.sw.js.map    1.3mb
  dist/boot.js.map        3.6kb

⚡ Done in 236ms

以下のコマンドでウェブサーバーを実行し、 http://localhost:8080/boot.html にアクセスすれば Redmine の読み込みが始まります [12] 。 なお、 pwa/dist ディレクトリ全体で 78MB 程度(1ファイル最大 20MB 程度)です。

12. HTTP ヘッダーの Set-Cookie や Location が小文字の場合は pwa/src/rails.sw.js の該当箇所を変更してください。

ruby -run -e httpd ./pwa/dist
[2024-09-27 13:33:34] INFO  WEBrick 1.8.2
[2024-09-27 13:33:34] INFO  ruby 3.4.0 (2024-05-16) [arm64-darwin23]
[2024-09-27 13:33:34] INFO  WEBrick::HTTPServer#start: pid=40115 port=8080
[2024-09-27 13:33:34] INFO  To access this server, open this URL in a browser:
[2024-09-27 13:33:34] INFO      http://[::1]:8080
[2024-09-27 13:33:34] INFO      http://127.0.0.1:8080

おわりに

今回は、 ruby/ruby.wasm を使用して、Redmine をブラウザ内で動作させる検証を行いました。
Ruby on Rails アプリケーション全体を Wasm 化した際の活用場面としては、 RubyGems on ruby.wasm - Speaker Deck によると、静的サイトとしてホスティングできるため、運用コストが低減されることや、セキュリティリスクが抑えられる点が挙げられます。具体例としては、プルリクエストのプレビュー環境などに利用できるのではと考えられています。

個人的な考えとしては、Redmine の Wiki 編集時のプレビュー機能において、ruby/ruby.wasm を利用し、一部の機能を Wasm 化することで、効率の向上が期待できると考えています。このアプローチにより、アプリケーションサーバーやデータベースサーバーとの通信を減らし、ユーザーへの応答速度が向上する可能性があります。

さらに、Redmine のプラグインに関しても、ruby/ruby.wasm を用いてパッケージ化することで、プラグインの配布やアップデートがより簡便になるのではと考えています。

【スタッフ募集中】
弊社ではAWSを活用したソリューションの企画・設計・構築・運用や、Ruby on Rails・JavaScriptフレームワークなどを使用したアプリケーション開発を行うスタッフを募集しています。採用情報の詳細
弊社での勤務に関心をお持ちの方は、知り合いの弊社社員・関係者を通じてご連絡ください。

My Redmine

こちらの記事もオススメです!
Turbo FramesでRedmineのフォーラム機能の画面遷移を削減できるか試した話
turbo-rails gemを介してTurbo Framesを利用し、フォーラムのメッセージをシームレスに更新できるかを検証しました。
続たのしい自作キーボード〜PiPi GherkinビルドログとRedmineショートカットキー〜
PiPi Gherkin + PRK Firmwareの自作キーボードを作りました。
ゲオあれこれレンタルでカメラをレンタルしてみた
家電の短期間のレンタルは、購入検討中のものを試すのに便利です。
5年ぶりの台湾訪問とセキュリティカンファレンスHITCON Community 2024に参加した話
アジア最大規模のセキュリティカンファレンスHITCON CMT 2024に参加しました。
My Redmine Global Edition と My Redmine JP Edition の違い
全世界向けの「My Redmine Global Edition」と⽇本国内向けの「My Redmine JP Edition」のサービスの違いを紹介します。
ファーエンドテクノロジーからのお知らせ(2024/10/02更新)
2024年10月19日 オライリー本の全冊公開日のお知らせ(もくもく勉強会も同時開催)
ファーエンドテクノロジーが所蔵するオライリー本(全冊)公開日のご案内です。公開日には「もくもく勉強会」も同時開催します。
RubyWorld Conference 2024 (12/5・6開催) にPlatinumスポンサーとして協賛
ファーエンドテクノロジー株式会社は、2024年12月5日(木)〜6日(金)に島根県松江市で開催される「RubyWorld Conference 2024」にPlatinumスポンサーとして協賛しています。
プロジェクト管理ツールRedmineのクラウドサービス「My Redmine」の海外向けサービス「My Redmine Global Edition」の提供を開始
「My Redmine」の海外向けサービスとして、新たに「My Redmine Global Edition」の提供を開始しました。
Redmineの最新情報をメールでお知らせする「Redmine News」配信中
新バージョンやセキュリティ修正のリリース情報、そのほか最新情報を迅速にお届け