seesaw 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.
data/History.txt ADDED
@@ -0,0 +1,5 @@
1
+ == 0.1.0 / 2007-08-18
2
+
3
+ * 1 major enhancement
4
+ * Birthday!
5
+
data/Manifest.txt ADDED
@@ -0,0 +1,8 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ bin/seesaw
6
+ lib/seesaw/init.rb
7
+ lib/seesaw/mongrel_cluster_patch.rb
8
+ test/test_seesaw.rb
data/README.txt ADDED
@@ -0,0 +1,48 @@
1
+ seesaw
2
+ by Matt Allen and Max Muermann
3
+ (http://rubyforge.org/projects/rails-oceania)
4
+
5
+ == DESCRIPTION:
6
+
7
+ FIX (describe your package)
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * FIX (list of features or problems)
12
+
13
+ == SYNOPSIS:
14
+
15
+ FIX (code sample of usage)
16
+
17
+ == REQUIREMENTS:
18
+
19
+ * FIX (list of requirements)
20
+
21
+ == INSTALL:
22
+
23
+ sudo gem install seesaw
24
+
25
+ == LICENSE:
26
+
27
+ (The MIT License)
28
+
29
+ Copyright (c) 2007 FIX
30
+
31
+ Permission is hereby granted, free of charge, to any person obtaining
32
+ a copy of this software and associated documentation files (the
33
+ 'Software'), to deal in the Software without restriction, including
34
+ without limitation the rights to use, copy, modify, merge, publish,
35
+ distribute, sublicense, and/or sell copies of the Software, and to
36
+ permit persons to whom the Software is furnished to do so, subject to
37
+ the following conditions:
38
+
39
+ The above copyright notice and this permission notice shall be
40
+ included in all copies or substantial portions of the Software.
41
+
42
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
43
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
44
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
45
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
46
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
47
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
48
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/seesaw/init.rb'
6
+
7
+ Hoe.new('seesaw', Seesaw::VERSION) do |p|
8
+ p.rubyforge_name = 'seesaw'
9
+ p.author = 'Matt Allen, Max Muermann'
10
+ p.email = 'max@muermann.org'
11
+ p.summary = 'Ripple-restart a mongrel cluster with no downtime'
12
+ p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
13
+ p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
14
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
15
+ p.extra_deps << ['gem_plugin', '>= 0.2.2'] << ['mongrel', '>= 1.0.1'] << ['mongrel_cluster', '>= 1.0.2']
16
+ p.spec_extras['autorequire'] = 'seesaw/init.rb'
17
+ end
18
+
19
+ # vim: syntax=Ruby
data/bin/seesaw ADDED
File without changes
@@ -0,0 +1,107 @@
1
+ require 'gem_plugin'
2
+ require 'mongrel'
3
+ require 'yaml'
4
+ require 'fileutils'
5
+ require 'seesaw/mongrel_cluster_patch'
6
+
7
+ module Seesaw
8
+ VERSION = '0.1.0'
9
+
10
+ module CommandBase
11
+ def symlink(cluster)
12
+ p "symlink to #{cluster}"
13
+ file = File.join(@config_path, @config_files[cluster])
14
+ p "target: #{file}"
15
+ FileUtils.ln_sf( file, @config_symlink)
16
+ end
17
+
18
+ def restart_mongrels(cluster=nil)
19
+ p "restart mongrels #{cluster}"
20
+ stop_mongrels(cluster)
21
+ start_mongrels(cluster)
22
+ end
23
+
24
+ def start_mongrels(cluster=nil)
25
+ p "start mongrels #{cluster}"
26
+ cmd = "mongrel_rails seesaw::start"
27
+ cmd << " --cluster #{cluster}" if cluster
28
+ system(cmd)
29
+ end
30
+
31
+ def stop_mongrels(cluster=nil)
32
+ p "stop mongrels #{cluster}"
33
+ cmd = "mongrel_rails seesaw::stop"
34
+ cmd << " --cluster #{cluster}" if cluster
35
+ system(cmd)
36
+ end
37
+
38
+ def restart_http(cluster=nil)
39
+ p "nginx restart"
40
+ system(@restart_cmd)
41
+ sleep 5
42
+ end
43
+
44
+ def shutdown_half_cluster(half)
45
+ p "shutdown half #{half}"
46
+ other_half = half == 1 ? 2 : 1
47
+ symlink(other_half)
48
+ restart_http
49
+ stop_mongrels(half)
50
+ end
51
+
52
+ def switch_to_half_cluster(half)
53
+ p "switch_to_half_cluster #{half}"
54
+ other_half = half == 1 ? 2 : 1
55
+ shutdown_half_cluster(other_half)
56
+ start_mongrels(other_half)
57
+ end
58
+
59
+ def start_cluster
60
+ p "start cluster"
61
+ symlink("all")
62
+ restart_http
63
+ end
64
+
65
+ def parse_options
66
+ @options = YAML.load(File.read(@config_file))
67
+ @config_path = @options["config_path"] || "/opt/local/etc/apache"
68
+ @config_files = @options["config_files"] || {}
69
+ @config_files[1] ||= "cluster_1.conf"
70
+ @config_files[2] ||= "cluster_2.conf"
71
+ @config_files["all"] ||= "cluster_all.conf"
72
+ @config_symlink = @options["config_symlink"] || "cluster.conf"
73
+ @config_symlink = File.join(@config_path, @config_symlink)
74
+ @restart_cmd = @options["restart_cmd"] || "apachectl graceful"
75
+ end
76
+
77
+ def validate
78
+ valid_exists?(@config_file, "Configuration file does not exist. Run mongrel_rails seesaw::configure.")
79
+ return @valid
80
+ end
81
+
82
+ def configure
83
+ options [
84
+ ['-C', '--config PATH', "Path to cluster configuration file", :@config_file, "config/seesaw.yml"],
85
+ ]
86
+ end
87
+
88
+ end
89
+
90
+ class Migrate < GemPlugin::Plugin "/commands"
91
+ include Mongrel::Command::Base
92
+ include CommandBase
93
+
94
+ def run
95
+ parse_options
96
+ p "c1"
97
+ switch_to_half_cluster(1)
98
+ p "c2"
99
+ switch_to_half_cluster(2)
100
+ p "nighty night"
101
+ sleep 20
102
+ p "all"
103
+ start_cluster
104
+ p "done"
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,368 @@
1
+ require 'gem_plugin'
2
+ require 'mongrel'
3
+ require 'yaml'
4
+ require 'enumerator'
5
+
6
+ module Seesaw
7
+ module ExecBase
8
+ include Mongrel::Command::Base
9
+
10
+ STATUS_OK = 0
11
+ STATUS_ERROR = 2
12
+
13
+ def validate
14
+ valid_exists?(@config_file, "Configuration file does not exist. Run mongrel_rails cluster::configure.")
15
+ return @valid
16
+ end
17
+
18
+ def read_options
19
+ @options = {
20
+ "environment" => ENV['RAILS_ENV'] || "development",
21
+ "port" => 3000,
22
+ "pid_file" => "tmp/pids/mongrel.pid",
23
+ "log_file" => "log/mongrel.log",
24
+ "servers" => 2
25
+ }
26
+ conf = YAML.load_file(@config_file)
27
+ @options.merge! conf if conf
28
+
29
+ process_pid_file @options["pid_file"]
30
+ process_log_file @options["log_file"]
31
+
32
+ start_port = end_port = @only
33
+ start_port ||= @options["port"].to_i
34
+ end_port ||= start_port + @options["servers"] - 1
35
+ @ports = (start_port..end_port).to_a
36
+ end
37
+
38
+ def process_pid_file(pid_file)
39
+ @pid_file_ext = File.extname(pid_file)
40
+ @pid_file_base = File.basename(pid_file, @pid_file_ext)
41
+ @pid_file_dir = File.dirname(pid_file)
42
+ end
43
+
44
+ def process_log_file(log_file)
45
+ @log_file_ext = File.extname(log_file)
46
+ @log_file_base = File.basename(log_file, @log_file_ext)
47
+ @log_file_dir = File.dirname(log_file)
48
+ end
49
+
50
+ def port_pid_file(port)
51
+ pid_file = [@pid_file_base, port].join(".") + @pid_file_ext
52
+ File.join(@pid_file_dir, pid_file)
53
+ end
54
+
55
+ def port_log_file(port)
56
+ log_file = [@log_file_base, port].join(".") + @log_file_ext
57
+ File.join(@log_file_dir, log_file)
58
+ end
59
+
60
+ def start
61
+ argv = [ "mongrel_rails" ]
62
+ argv << "start"
63
+ argv << "-d"
64
+ argv << "-e #{@options["environment"]}" if @options["environment"]
65
+ argv << "-a #{@options["address"]}" if @options["address"]
66
+ argv << "-c #{@options["cwd"]}" if @options["cwd"]
67
+ argv << "-t #{@options["timeout"]}" if @options["timeout"]
68
+ argv << "-m #{@options["mime_map"]}" if @options["mime_map"]
69
+ argv << "-r #{@options["docroot"]}" if @options["docroot"]
70
+ argv << "-n #{@options["num_procs"]}" if @options["num_procs"]
71
+ argv << "-B" if @options["debug"]
72
+ argv << "-S #{@options["config_script"]}" if @options["config_script"]
73
+ argv << "--user #{@options["user"]}" if @options["user"]
74
+ argv << "--group #{@options["group"]}" if @options["group"]
75
+ argv << "--prefix #{@options["prefix"]}" if @options["prefix"]
76
+ cmd = argv.join " "
77
+
78
+ @ports.each do |port|
79
+ if @clean && pid_file_exists?(port) && !check_process(port)
80
+ pid_file = port_pid_file(port)
81
+ log "missing process: removing #{pid_file}"
82
+ File.unlink(pid_file)
83
+ end
84
+
85
+ if pid_file_exists?(port) && check_process(port)
86
+ log "already started port #{port}"
87
+ next
88
+ end
89
+
90
+ exec_cmd = cmd + " -p #{port} -P #{port_pid_file(port)}"
91
+ exec_cmd += " -l #{port_log_file(port)}"
92
+ log "starting port #{port}"
93
+ log_verbose exec_cmd
94
+ output = `#{exec_cmd}`
95
+ log_error output unless $?.success?
96
+ end
97
+ end
98
+
99
+ def stop
100
+ argv = [ "mongrel_rails" ]
101
+ argv << "stop"
102
+ argv << "-c #{@options["cwd"]}" if @options["cwd"]
103
+ argv << "-f" if @force
104
+ cmd = argv.join " "
105
+
106
+ @ports.each do |port|
107
+ pid = check_process(port)
108
+ if @clean && pid && !pid_file_exists?(port)
109
+ log "missing pid_file: killing mongrel_rails port #{port}, pid #{pid}"
110
+ Process.kill("KILL", pid.to_i)
111
+ end
112
+
113
+ if !check_process(port)
114
+ log "already stopped port #{port}"
115
+ next
116
+ end
117
+
118
+ exec_cmd = cmd + " -P #{port_pid_file(port)}"
119
+ log "stopping port #{port}"
120
+ log_verbose exec_cmd
121
+ output = `#{exec_cmd}`
122
+ log_error output unless $?.success?
123
+
124
+ end
125
+ end
126
+
127
+ def status
128
+ status = STATUS_OK
129
+
130
+ @ports.each do |port|
131
+ pid = check_process(port)
132
+ unless pid_file_exists?(port)
133
+ log "missing pid_file: #{port_pid_file(port)}"
134
+ status = STATUS_ERROR
135
+ else
136
+ log "found pid_file: #{port_pid_file(port)}"
137
+ end
138
+ if pid
139
+ log "found mongrel_rails: port #{port}, pid #{pid}"
140
+ else
141
+ log "missing mongrel_rails: port #{port}"
142
+ status = STATUS_ERROR
143
+ end
144
+ puts ""
145
+ end
146
+
147
+ return status
148
+ end
149
+
150
+ def pid_file_exists?(port)
151
+ pid_file = port_pid_file(port)
152
+ exists = false
153
+ chdir_cwd do
154
+ exists = File.exists?(pid_file)
155
+ end
156
+ exists
157
+ end
158
+
159
+ def check_process(port)
160
+ if pid_file_exists?(port)
161
+ pid = read_pid(port)
162
+ ps_output = `ps -o #{cmd_name}= -p #{pid}`
163
+ pid = ps_output =~ /mongrel_rails/ ? pid : nil
164
+ else
165
+ pid = find_pid(port)
166
+ end
167
+ return pid
168
+ end
169
+
170
+ def cmd_name
171
+ RUBY_PLATFORM =~ /solaris/i ? "args" : "command"
172
+ end
173
+
174
+ def chdir_cwd
175
+ pwd = Dir.pwd
176
+ Dir.chdir(@options["cwd"]) if @options["cwd"]
177
+ yield
178
+ Dir.chdir(pwd) if @options["cwd"]
179
+ end
180
+
181
+ def read_pid(port)
182
+ pid_file = port_pid_file(port)
183
+ pid = 0
184
+ chdir_cwd do
185
+ pid = File.read(pid_file)
186
+ end
187
+ return pid
188
+ end
189
+
190
+ def find_pid(port)
191
+ ps_cmd = "ps -ewwo pid,#{cmd_name}"
192
+ ps_output = `#{ps_cmd}`
193
+ ps_output.each do |line|
194
+ if line =~ /-P #{Regexp.escape(port_pid_file(port))} /
195
+ pid = line.split[0]
196
+ return pid
197
+ end
198
+ end
199
+ return nil
200
+ end
201
+
202
+ def log_error(message)
203
+ log(message)
204
+ end
205
+
206
+ def log_verbose(message)
207
+ log(message) if @verbose
208
+ end
209
+
210
+ def log(message)
211
+ puts message
212
+ end
213
+
214
+ def clustered &block
215
+ if @cluster
216
+ @only = nil
217
+ read_options
218
+ @ports.enum_slice((@ports.size/2.0).ceil).map{|x|x}[@cluster.to_i-1].each do |port|
219
+ @only = port
220
+ read_options
221
+ yield
222
+ end
223
+ else
224
+ read_options
225
+ yield
226
+ end
227
+ end
228
+
229
+ end
230
+ class Start < GemPlugin::Plugin "/commands"
231
+ include ExecBase
232
+
233
+ def configure
234
+ options [
235
+ ['-C', '--config PATH', "Path to cluster configuration file", :@config_file, "config/mongrel_cluster.yml"],
236
+ ['-v', '--verbose', "Print all called commands and output.", :@verbose, false],
237
+ ['', '--clean', "Remove pid_file if needed before starting", :@clean, false],
238
+ ['', '--only PORT', "Port number of cluster member", :@only, nil],
239
+ ['', '--cluster NUMBER', "1 or 2 - first half or second half of cluster", :@cluster, nil]
240
+ ]
241
+ end
242
+
243
+ def run
244
+ clustered {start}
245
+ end
246
+ end
247
+
248
+ class Stop < GemPlugin::Plugin "/commands"
249
+ include ExecBase
250
+
251
+ def configure
252
+ options [
253
+ ['-C', '--config PATH', "Path to cluster configuration file", :@config_file, "config/mongrel_cluster.yml"],
254
+ ['-f', '--force', "Force the shutdown.", :@force, false],
255
+ ['-v', '--verbose', "Print all called commands and output.", :@verbose, false],
256
+ ['', '--clean', "Remove orphaned process if needed before stopping", :@clean, false],
257
+ ['', '--only PORT', "Port number of cluster member", :@only, nil],
258
+ ['', '--cluster NUMBER', "1 or 2 - first half or second half of cluster", :@cluster, nil]
259
+ ]
260
+ end
261
+
262
+ def run
263
+ clustered {stop}
264
+ end
265
+ end
266
+
267
+ class Restart < GemPlugin::Plugin "/commands"
268
+ include ExecBase
269
+
270
+ def configure
271
+ options [
272
+ ['-C', '--config PATH', "Path to cluster configuration file", :@config_file, "config/mongrel_cluster.yml"],
273
+ ['-f', '--force', "Force the shutdown.", :@force, false],
274
+ ['-v', '--verbose', "Print all called commands and output.", :@verbose, false],
275
+ ['', '--clean', "Call stop and start with --clean", :@clean, false],
276
+ ['', '--only PORT', "Port number of cluster member", :@only, nil],
277
+ ['', '--cluster NUMBER', "1 or 2 - first half or second half of cluster", :@cluster, nil]
278
+ ]
279
+ end
280
+
281
+ def run
282
+ clustered do
283
+ stop
284
+ start
285
+ end
286
+ end
287
+
288
+ end
289
+
290
+ class Configure < GemPlugin::Plugin "/commands"
291
+ include ExecBase
292
+
293
+ def configure
294
+ options [
295
+ ["-e", "--environment ENV", "Rails environment to run as", :@environment, nil],
296
+ ['-p', '--port PORT', "Starting port to bind to", :@port, 3000],
297
+ ['-a', '--address ADDR', "Address to bind to", :@address, nil],
298
+ ['-l', '--log FILE', "Where to write log messages", :@log_file, "log/mongrel.log"],
299
+ ['-P', '--pid FILE', "Where to write the PID", :@pid_file, "tmp/pids/mongrel.pid"],
300
+ ['-c', '--chdir PATH', "Change to dir before starting (will be expanded)", :@cwd, nil],
301
+ ['-t', '--timeout SECONDS', "Timeout all requests after SECONDS time", :@timeout, nil],
302
+ ['-m', '--mime PATH', "A YAML file that lists additional MIME types", :@mime_map, nil],
303
+ ['-r', '--root PATH', "Set the document root (default 'public')", :@docroot, nil],
304
+ ['-n', '--num-procs INT', "Number of processor threads to use", :@num_procs, nil],
305
+ ['-B', '--debug', "Enable debugging mode", :@debug, nil],
306
+ ['-S', '--script PATH', "Load the given file as an extra config script.", :@config_script, nil],
307
+ ['-N', '--num-servers INT', "Number of Mongrel servers", :@servers, 2],
308
+ ['-C', '--config PATH', "Path to cluster configuration file", :@config_file, "config/mongrel_cluster.yml"],
309
+ ['', '--user USER', "User to run as", :@user, nil],
310
+ ['', '--group GROUP', "Group to run as", :@group, nil],
311
+ ['', '--prefix PREFIX', "Rails prefix to use", :@prefix, nil]
312
+ ]
313
+ end
314
+
315
+ def validate
316
+ @servers = @servers.to_i
317
+
318
+ valid?(@servers > 0, "Must give a valid number of servers")
319
+ valid_dir? File.dirname(@config_file), "Path to config file not valid: #{@config_file}"
320
+
321
+ return @valid
322
+ end
323
+
324
+ def run
325
+ @options = {
326
+ "port" => @port,
327
+ "servers" => @servers,
328
+ "pid_file" => @pid_file
329
+ }
330
+
331
+ @options["log_file"] = @log_file if @log_file
332
+ @options["debug"] = @debug if @debug
333
+ @options["num_procs"] = @num_procs if @num_procs
334
+ @options["docroot"] = @docroot if @docroots
335
+ @options["address"] = @address if @address
336
+ @options["timeout"] = @timeout if @timeout
337
+ @options["environment"] = @environment if @environment
338
+ @options["mime_map"] = @mime_map if @mime_map
339
+ @options["config_script"] = @config_script if @config_script
340
+ @options["cwd"] = @cwd if @cwd
341
+ @options["user"] = @user if @user
342
+ @options["group"] = @group if @group
343
+ @options["prefix"] = @prefix if @prefix
344
+
345
+ log "Writing configuration file to #{@config_file}."
346
+ File.open(@config_file,"w") {|f| f.write(@options.to_yaml)}
347
+ end
348
+ end
349
+
350
+ class Status < GemPlugin::Plugin "/commands"
351
+ include ExecBase
352
+
353
+ def configure
354
+ options [
355
+ ['-C', '--config PATH', "Path to cluster configuration file", :@config_file, "config/mongrel_cluster.yml"],
356
+ ['-v', '--verbose', "Print all called commands and output.", :@verbose, false],
357
+ ['', '--only PORT', "Port number of cluster member", :@only, nil],
358
+ ['', '--cluster NUMBER', "1 or 2 - first half or second half of cluster", :@cluster, nil]
359
+ ]
360
+ end
361
+
362
+ def run
363
+ clustered {status}
364
+ end
365
+
366
+ end
367
+ end
368
+
File without changes
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.4
3
+ specification_version: 1
4
+ name: seesaw
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.0
7
+ date: 2007-08-18 00:00:00 +10:00
8
+ summary: Ripple-restart a mongrel cluster with no downtime
9
+ require_paths:
10
+ - lib
11
+ email: max@muermann.org
12
+ homepage: " by Matt Allen and Max Muermann"
13
+ rubyforge_project: seesaw
14
+ description: "== FEATURES/PROBLEMS: * FIX (list of features or problems) == SYNOPSIS: FIX (code sample of usage) == REQUIREMENTS:"
15
+ autorequire: seesaw/init.rb
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Matt Allen, Max Muermann
31
+ files:
32
+ - History.txt
33
+ - Manifest.txt
34
+ - README.txt
35
+ - Rakefile
36
+ - bin/seesaw
37
+ - lib/seesaw/init.rb
38
+ - lib/seesaw/mongrel_cluster_patch.rb
39
+ - test/test_seesaw.rb
40
+ test_files:
41
+ - test/test_seesaw.rb
42
+ rdoc_options:
43
+ - --main
44
+ - README.txt
45
+ extra_rdoc_files:
46
+ - History.txt
47
+ - Manifest.txt
48
+ - README.txt
49
+ executables:
50
+ - seesaw
51
+ extensions: []
52
+
53
+ requirements: []
54
+
55
+ dependencies:
56
+ - !ruby/object:Gem::Dependency
57
+ name: gem_plugin
58
+ version_requirement:
59
+ version_requirements: !ruby/object:Gem::Version::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 0.2.2
64
+ version:
65
+ - !ruby/object:Gem::Dependency
66
+ name: mongrel
67
+ version_requirement:
68
+ version_requirements: !ruby/object:Gem::Version::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 1.0.1
73
+ version:
74
+ - !ruby/object:Gem::Dependency
75
+ name: mongrel_cluster
76
+ version_requirement:
77
+ version_requirements: !ruby/object:Gem::Version::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 1.0.2
82
+ version:
83
+ - !ruby/object:Gem::Dependency
84
+ name: hoe
85
+ version_requirement:
86
+ version_requirements: !ruby/object:Gem::Version::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: 1.2.2
91
+ version: