dragonruby-rack 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4eb445a0e6d8d213740fb5604c1dbd3794d92732a540d5157837fee9f22a4314
4
+ data.tar.gz: 4957d3f7413f65f43ee11f236cb8bfe892a4b7aac958493837eb8985b79219c0
5
+ SHA512:
6
+ metadata.gz: fd9e975ed8ef7a749e9f67a8f14db04b4c8fe7ff621097977471197a40fc848d5317ee76656ad677f3f7c3362fd31975529189d4ad410fc76211b2d9e63d4534
7
+ data.tar.gz: b0763b9f6ac8f1ffe3bc16d015a8a113dcb1160d88df5d62903351d05cd3faa5ab9fb77492ba5298ce1fc233bddc7fb69ac5ad75352030a1af281ed9537a85c9
data/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-08-02
4
+
5
+ Extracted from https://stammer.dev website, where it had been serving
6
+ [Underwater](https://github.com/rstammer/underwater) inside of a classical Rails app.
7
+
8
+ - `DragonRuby::Rack::Game` — mountable endpoint: cross-origin isolation headers
9
+ on the document and every subresource, `<base>` injection so relative assets
10
+ resolve under any mount path, `content-length` corrected for the longer body.
11
+ - A bare mount path (empty `PATH_INFO`, which is what `Rack::URLMap` hands a
12
+ mounted app) now serves the index. It used to 404 in plain Rack while working
13
+ under Rails, which normalizes the path first.
14
+ - `DragonRuby::Rack::Bundle.install` and `rake dragonruby:install` — replace the
15
+ bundle directory from a published zip, refusing any destination that is not
16
+ empty and holds no `index.html`, and verifying the result has an `index.html`
17
+ and a `.wasm`.
18
+ - `rails g dragonruby:mount` — writes the mount into `config/routes.rb`.
19
+ - Railtie loads only when Rails is present; Rails is not a dependency.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Robin Stammer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # dragonruby-rack
2
+
3
+ Serve a [DragonRuby GTK](https://dragonruby.org) HTML5 export from a Rack or
4
+ Rails app, on your own domain, without an iframe and without itch.io.
5
+
6
+ ```ruby
7
+ # Gemfile
8
+ gem "dragonruby-rack"
9
+
10
+ # config/routes.rb
11
+ mount DragonRuby::Rack::Game.new(Rails.root.join("games/underwater")) => "/underwater"
12
+ ```
13
+
14
+ That is the whole thing. The game is then at `/underwater/`.
15
+
16
+ Built for and running on [stammer.dev/underwater](https://stammer.dev/underwater/).
17
+
18
+ ## Why this is not just a static directory
19
+
20
+ DragonRuby's WASM runtime allocates a `SharedArrayBuffer`, and browsers hand
21
+ that out only to a **cross-origin isolated** document — one served with both
22
+
23
+ ```
24
+ Cross-Origin-Opener-Policy: same-origin
25
+ Cross-Origin-Embedder-Policy: require-corp
26
+ ```
27
+
28
+ Rails' static file server runs *before* the router and cannot set headers per
29
+ path, so a bundle dropped into `public/` is served without them and the game
30
+ never boots. Hence: keep the bundle outside `public/`, serve it through this
31
+ endpoint, which sets the headers on the document **and on every subresource** —
32
+ `require-corp` applies to the `.wasm` too.
33
+
34
+ The second thing it does is less obvious. The export links its assets
35
+ relatively (`game.css`, `gamedata/…`), which under a mount only resolve if the
36
+ document URL ends in a slash — and nothing can guarantee that, since Rails
37
+ normalizes the path away before the endpoint sees it and a redirect onto the
38
+ slash would be a loop. So the endpoint injects a `<base href="/underwater/">`
39
+ into the served HTML, and the assets resolve either way.
40
+
41
+ ## Without Rails
42
+
43
+ It is a plain Rack app; Rails is not a dependency.
44
+
45
+ ```ruby
46
+ # config.ru
47
+ require "dragonruby-rack"
48
+ run DragonRuby::Rack::Game.new("builds/underwater-html5")
49
+ ```
50
+
51
+ or mounted:
52
+
53
+ ```ruby
54
+ require "dragonruby-rack"
55
+
56
+ app = Rack::Builder.new do
57
+ map("/underwater") { run DragonRuby::Rack::Game.new("builds/underwater-html5") }
58
+ end
59
+ ```
60
+
61
+ ## Getting a bundle in
62
+
63
+ Export the game (`dragonruby-publish --platforms=html5 --package …`) and unpack
64
+ the zip where the mount points:
65
+
66
+ ```sh
67
+ rake dragonruby:install[../underwater/builds/underwater-html5.zip,underwater]
68
+ ```
69
+
70
+ The task **replaces** the directory rather than unpacking over it — a file that
71
+ existed in the old version and not in the new one would otherwise stay behind
72
+ and be served for good. Because that is a delete, it refuses any destination
73
+ that is not empty and holds no `index.html`, and it checks afterwards that what
74
+ came out has an `index.html` and a `.wasm` in it (a desktop export unzips
75
+ perfectly well and then serves nothing). `FORCE=1` overrides the refusal.
76
+
77
+ `config.dragonruby.games_root` sets where bundles live; the default is `games`.
78
+
79
+ ## Generator
80
+
81
+ ```sh
82
+ rails g dragonruby:mount underwater
83
+ ```
84
+
85
+ Creates `games/underwater/` and writes the mount into `config/routes.rb`.
86
+ `--path=` and `--at=` override the directory and the URL. Running it twice does
87
+ not mount the path twice.
88
+
89
+ ## What this gem is not
90
+
91
+ It does **not** contain DragonRuby, and it does not build your game. DragonRuby
92
+ is commercial software with its own licence; you need your own copy to produce
93
+ the export this serves. This gem only takes the finished directory and puts it
94
+ on the web correctly.
95
+
96
+ ## Development
97
+
98
+ ```sh
99
+ bundle install
100
+ bundle exec rspec
101
+ ```
102
+
103
+ ## Licence
104
+
105
+ MIT. See LICENSE.txt.
@@ -0,0 +1,86 @@
1
+ require "fileutils"
2
+
3
+ module DragonRuby
4
+ module Rack
5
+ # Puts a published HTML5 export where the app can serve it.
6
+ #
7
+ # This is the step that used to be three shell lines, one of which was
8
+ # `rm -rf` against a path typed by hand. Replacing the directory rather than
9
+ # unpacking over it is not optional — a file deleted between two versions
10
+ # would otherwise stay behind and be served forever. So the delete has to
11
+ # happen, and the only useful thing to do about it is to refuse to delete
12
+ # anything that is not already a bundle.
13
+ module Bundle
14
+ class Error < StandardError; end
15
+
16
+ # What every DragonRuby HTML5 export has in it, and what a directory must
17
+ # therefore look like before this is willing to erase it.
18
+ MARKER = "index.html"
19
+
20
+ class << self
21
+ # Unpacks +zip+ into +into+, replacing whatever bundle is there.
22
+ #
23
+ # Refuses when +into+ exists but holds no index.html: that is either the
24
+ # wrong path or something that is not a bundle, and both are worth an
25
+ # error rather than a deletion. Pass force: true to say you meant it.
26
+ def install(zip, into, force: false)
27
+ zip = File.expand_path(zip.to_s)
28
+ into = File.expand_path(into.to_s)
29
+
30
+ raise Error, "no such zip: #{zip}" unless File.file?(zip)
31
+
32
+ guard_destination(into, force: force)
33
+
34
+ FileUtils.rm_rf(into)
35
+ FileUtils.mkdir_p(into)
36
+ unzip(zip, into)
37
+ verify(into)
38
+
39
+ into
40
+ end
41
+
42
+ private
43
+
44
+ def guard_destination(into, force:)
45
+ return if force
46
+ return unless File.exist?(into)
47
+
48
+ unless File.directory?(into)
49
+ raise Error, "#{into} exists and is not a directory"
50
+ end
51
+
52
+ return if File.exist?(File.join(into, MARKER))
53
+ return if Dir.children(into).empty?
54
+
55
+ raise Error, <<~MESSAGE.strip
56
+ #{into} is not empty and holds no #{MARKER}, so it does not look like
57
+ a game bundle. Refusing to delete it. Pass force: true (or
58
+ FORCE=1 to the rake task) if this really is the right directory.
59
+ MESSAGE
60
+ end
61
+
62
+ # Shelling out to unzip keeps rubyzip out of the dependency list for a
63
+ # step that runs once per release on a developer machine. If it is ever
64
+ # needed somewhere without unzip, that is the moment to reconsider —
65
+ # not before.
66
+ def unzip(zip, into)
67
+ ok = system("unzip", "-q", zip, "-d", into)
68
+ raise Error, "unzip failed for #{zip}" unless ok
69
+ end
70
+
71
+ # A zip that unpacked without error can still be the wrong zip. Both
72
+ # halves of a DragonRuby export have to be there or the game shows a
73
+ # blank page in production and nowhere else.
74
+ def verify(into)
75
+ unless File.exist?(File.join(into, MARKER))
76
+ raise Error, "no #{MARKER} in the unpacked bundle at #{into}"
77
+ end
78
+
79
+ return if Dir.glob(File.join(into, "**", "*.wasm")).any?
80
+
81
+ raise Error, "no .wasm in the unpacked bundle at #{into} — is this an HTML5 export?"
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,94 @@
1
+ require "rack"
2
+
3
+ module DragonRuby
4
+ module Rack
5
+ # Serves a DragonRuby GTK HTML5 export as a self-contained, cross-origin
6
+ # isolated static bundle, mountable at any path in a Rack or Rails app:
7
+ #
8
+ # mount DragonRuby::Rack::Game.new("games/underwater") => "/underwater"
9
+ #
10
+ # Why this exists: DragonRuby's WASM runtime uses SharedArrayBuffer, which
11
+ # the browser only hands out to a *cross-origin isolated* document — one
12
+ # served with both COOP and COEP. Rails' public/ file server
13
+ # (ActionDispatch::Static) runs before the router and cannot set these
14
+ # headers per path, which is why the bundle belongs outside public/ and is
15
+ # served exclusively through this endpoint.
16
+ #
17
+ # Everything in here refers to the real Rack as ::Rack. Inside
18
+ # DragonRuby::Rack, a bare `Rack::Static` resolves to
19
+ # DragonRuby::Rack::Static and raises NameError — the one price of naming
20
+ # the module after the gem.
21
+ class Game
22
+ # The two headers that put the document into cross-origin isolation.
23
+ # Without them the WASM runtime cannot allocate its SharedArrayBuffer and
24
+ # refuses to boot. Same-origin subresources (all of the bundle) satisfy
25
+ # require-corp without a per-file Cross-Origin-Resource-Policy header.
26
+ ISOLATION_HEADERS = {
27
+ "cross-origin-opener-policy" => "same-origin",
28
+ "cross-origin-embedder-policy" => "require-corp",
29
+ }.freeze
30
+
31
+ NOT_FOUND = ->(_env) { [404, { "content-type" => "text/plain" }, ["Not found"]] }
32
+
33
+ attr_reader :root
34
+
35
+ # A missing directory is deliberately not an error. Routes are drawn at
36
+ # boot, and a fresh clone has no build in it yet; raising here would stop
37
+ # the whole app from starting over a game that has not been exported.
38
+ # Requests simply 404 until the bundle is there.
39
+ def initialize(root, index: "index.html")
40
+ @root = root.to_s
41
+ @static = ::Rack::Static.new(
42
+ NOT_FOUND,
43
+ root: @root,
44
+ urls: [""], # serve every path under this mount from the bundle
45
+ index: index, # directory requests ("/") resolve to index.html
46
+ )
47
+ end
48
+
49
+ def call(env)
50
+ env = normalize_root(env)
51
+ status, headers, body = @static.call(env)
52
+ ISOLATION_HEADERS.each { |key, value| headers[key] = value }
53
+ body = rebase_html(body, headers, env) if html?(headers)
54
+ [status, headers, body]
55
+ end
56
+
57
+ private
58
+
59
+ # A request for the mount point itself arrives with an empty PATH_INFO
60
+ # under Rack::URLMap, and Rack::Static only resolves its index for a path
61
+ # ending in "/". So a plain Rack mount answered the bare URL with 404
62
+ # while the same mount under Rails worked, because Rails normalizes the
63
+ # path to "/" before the endpoint sees it. Same request, same app, two
64
+ # answers — worth removing rather than documenting.
65
+ def normalize_root(env)
66
+ return env unless env["PATH_INFO"].to_s.empty?
67
+
68
+ env.merge("PATH_INFO" => "/")
69
+ end
70
+
71
+ def html?(headers)
72
+ headers["content-type"].to_s.include?("text/html")
73
+ end
74
+
75
+ # The bundle's index.html links its assets relatively (game.css, the
76
+ # loader, gamedata/). Mounted under a subpath, those only resolve if the
77
+ # document URL ends in a slash — which cannot be forced, since Rails
78
+ # normalizes the path away before the mount sees it, and a redirect onto
79
+ # the slash would be a loop. Injecting a <base> pointing at the mount root
80
+ # makes every relative URL resolve either way.
81
+ def rebase_html(body, headers, env)
82
+ html = +""
83
+ body.each { |chunk| html << chunk }
84
+ body.close if body.respond_to?(:close)
85
+ return [html] if html.empty? # HEAD carries no body — leave headers as-is
86
+
87
+ base = "#{env['SCRIPT_NAME']}/"
88
+ html = html.sub(/<head>/i, %(<head><base href="#{base}">))
89
+ headers["content-length"] = html.bytesize.to_s
90
+ [html]
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,21 @@
1
+ require "rails/railtie"
2
+
3
+ module DragonRuby
4
+ module Rack
5
+ # Everything Rails-specific in the gem is here and in lib/generators. The
6
+ # railtie is loaded only when Rails is already in the process (see
7
+ # lib/dragonruby/rack.rb), so requiring this file is what makes the gem a
8
+ # Rails gem — nothing else in it knows about Rails at all.
9
+ class Railtie < ::Rails::Railtie
10
+ # config.dragonruby.games_root — where bundles live, relative to the app
11
+ # root. Only the rake task and the generator read it; the Rack app itself
12
+ # takes a plain path and has no configuration.
13
+ config.dragonruby = ActiveSupport::OrderedOptions.new
14
+ config.dragonruby.games_root = "games"
15
+
16
+ rake_tasks do
17
+ load File.expand_path("../../tasks/dragonruby.rake", __dir__)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ module DragonRuby
2
+ module Rack
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,13 @@
1
+ require_relative "rack/version"
2
+ require_relative "rack/game"
3
+ require_relative "rack/bundle"
4
+
5
+ # The Rails half is optional on purpose: the gem is a Rack gem, and a Sinatra or
6
+ # plain-Rack user should not pull a framework in behind it. Loading the railtie
7
+ # only when Rails is already present keeps that promise.
8
+ require_relative "rack/railtie" if defined?(::Rails::Railtie)
9
+
10
+ module DragonRuby
11
+ module Rack
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ # The gem is called dragonruby-rack, so `gem "dragonruby-rack"` in a Gemfile
2
+ # requires this path by default. The code lives under lib/dragonruby/rack.
3
+ require_relative "dragonruby/rack"
@@ -0,0 +1,82 @@
1
+ require "rails/generators"
2
+
3
+ module DragonRuby
4
+ module Generators
5
+ # rails g dragonruby:mount underwater
6
+ #
7
+ # Writes the one line that is easy to get subtly wrong: the mount has to
8
+ # point at a directory outside public/, and the game is linked with a
9
+ # trailing slash.
10
+ #
11
+ # The namespace is declared rather than derived. Rails would camelize
12
+ # "dragonruby" to Dragonruby and look for that constant; the gem's module is
13
+ # DragonRuby, so the two are spelled out separately here.
14
+ class MountGenerator < ::Rails::Generators::Base
15
+ namespace "dragonruby:mount"
16
+
17
+ argument :name, type: :string,
18
+ desc: "Name of the game — becomes both the directory and the URL path"
19
+
20
+ class_option :path, type: :string, default: nil,
21
+ desc: "Directory holding the bundle (default: <games_root>/<name>)"
22
+ class_option :at, type: :string, default: nil,
23
+ desc: "URL path to mount at (default: /<name>)"
24
+
25
+ def create_bundle_directory
26
+ empty_directory bundle_path
27
+ create_file File.join(bundle_path, ".keep"), "" unless File.exist?(File.join(destination_root, bundle_path, ".keep"))
28
+ end
29
+
30
+ def add_the_mount
31
+ routes = File.join(destination_root, "config/routes.rb")
32
+ unless File.exist?(routes)
33
+ say_status :skip, "config/routes.rb not found — add this yourself:\n#{mount_line}", :yellow
34
+ return
35
+ end
36
+
37
+ if File.read(routes).include?(%("#{url_path}"))
38
+ say_status :identical, "#{url_path} is already mounted in config/routes.rb", :blue
39
+ return
40
+ end
41
+
42
+ inject_into_file "config/routes.rb", comment + mount_line + "\n",
43
+ after: /Rails\.application\.routes\.draw do\n/
44
+ end
45
+
46
+ def tell_them_what_is_missing
47
+ say_status :next, "put an HTML5 export into #{bundle_path}, e.g.:", :green
48
+ say " rake dragonruby:install[path/to/#{name}-html5.zip,#{name}]"
49
+ say_status :link, "then the game is at #{url_path}/ (the trailing slash matters in links)", :green
50
+ end
51
+
52
+ private
53
+
54
+ def bundle_path
55
+ options[:path] || File.join(games_root, name)
56
+ end
57
+
58
+ def games_root
59
+ ::Rails.application.config.dragonruby.games_root
60
+ rescue StandardError
61
+ "games"
62
+ end
63
+
64
+ def url_path
65
+ options[:at] || "/#{name}"
66
+ end
67
+
68
+ def mount_line
69
+ %( mount DragonRuby::Rack::Game.new(Rails.root.join("#{bundle_path}")) => "#{url_path}"\n)
70
+ end
71
+
72
+ def comment
73
+ <<~RUBY
74
+ # #{name} — a DragonRuby HTML5 export, served cross-origin isolated so its
75
+ # WASM runtime can use SharedArrayBuffer. The bundle lives outside public/
76
+ # on purpose: Rails' static file server runs before the router and would
77
+ # serve it without those headers. Link to it as #{url_path}/
78
+ RUBY
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,20 @@
1
+ require "dragonruby/rack/bundle"
2
+
3
+ namespace :dragonruby do
4
+ desc "Install a published HTML5 export: rake dragonruby:install[path/to/game-html5.zip,underwater]"
5
+ task :install, [:zip, :name] => :environment do |_task, args|
6
+ zip = args[:zip] or abort "usage: rake dragonruby:install[path/to/game-html5.zip,name]"
7
+ name = args[:name] or abort "usage: rake dragonruby:install[path/to/game-html5.zip,name]"
8
+
9
+ games_root = Rails.application.config.dragonruby.games_root
10
+ into = Rails.root.join(games_root, name)
11
+
12
+ path = DragonRuby::Rack::Bundle.install(zip, into, force: ENV["FORCE"] == "1")
13
+ size = Dir.glob(File.join(path, "**", "*")).sum { |f| File.file?(f) ? File.size(f) : 0 }
14
+
15
+ puts "Installed #{File.basename(zip)} into #{into} (#{(size / 1_048_576.0).round(1)} MB)"
16
+ puts "Mount it with: mount DragonRuby::Rack::Game.new(Rails.root.join(\"#{games_root}/#{name}\")) => \"/#{name}\""
17
+ rescue DragonRuby::Rack::Bundle::Error => e
18
+ abort "dragonruby:install — #{e.message}"
19
+ end
20
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dragonruby-rack
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Robin Stammer
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rack
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '2.2'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '4'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '2.2'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '4'
32
+ description: |-
33
+ A mountable Rack endpoint that serves a DragonRuby GTK HTML5 export with the
34
+ cross-origin isolation headers its WASM runtime needs, and rebases the
35
+ bundle's relative asset links so it works under any mount path. Ships an
36
+ optional Rails generator and rake task; the Rack app itself needs no Rails.
37
+ email:
38
+ - robin.stammer@posteo.de
39
+ executables: []
40
+ extensions: []
41
+ extra_rdoc_files: []
42
+ files:
43
+ - CHANGELOG.md
44
+ - LICENSE.txt
45
+ - README.md
46
+ - lib/dragonruby-rack.rb
47
+ - lib/dragonruby/rack.rb
48
+ - lib/dragonruby/rack/bundle.rb
49
+ - lib/dragonruby/rack/game.rb
50
+ - lib/dragonruby/rack/railtie.rb
51
+ - lib/dragonruby/rack/version.rb
52
+ - lib/generators/dragonruby/mount/mount_generator.rb
53
+ - lib/tasks/dragonruby.rake
54
+ homepage: https://github.com/rstammer/dragonruby-rack
55
+ licenses:
56
+ - MIT
57
+ metadata:
58
+ homepage_uri: https://github.com/rstammer/dragonruby-rack
59
+ changelog_uri: https://github.com/rstammer/dragonruby-rack/blob/main/CHANGELOG.md
60
+ rubygems_mfa_required: 'true'
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '3.1'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.6.9
76
+ specification_version: 4
77
+ summary: Serve a DragonRuby GTK HTML5 export from a Rack or Rails app
78
+ test_files: []