rails-app-installer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,32 @@
1
+ This is an installer for Rails applications. It's designed to allow users to
2
+ install Rails apps onto their own systems with a minimum amount of effort.
3
+
4
+ This installer was originally part of Typo (http://typosphere.org).
5
+
6
+ Adding the installer to your Rails app
7
+ --------------------------------------
8
+
9
+ To add the installer to your application, copy the `installer` file from the
10
+ examples directory into your project's `bin` directory, and rename it to match
11
+ your application name. For example, Typo's installer lives in `bin/typo`. Then
12
+ modify your installer as needed, customizing the application name, support
13
+ address, and required Rails version. Once this is complete, create an
14
+ `installer` directory for your app and copy
15
+ `examples/rails_installer_defaults.yml` into it. You can probably use it
16
+ unchanged.
17
+
18
+ You should now be able to test the installer like this:
19
+
20
+ $ ruby ./bin/my_installer install /tmp/foo cwd
21
+
22
+ This will try to install your app in `/tmp/foo` using the current directory as
23
+ a template. If you leave off the `cwd` option at the end, then the installer will look for the most recent Ruby GEM for your app, using the `application_name` line from the installer as the gem name.
24
+
25
+ Creating a Gem
26
+ --------------
27
+
28
+
29
+
30
+ Using the installer
31
+ -------------------
32
+
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'rails-installer'
5
+
6
+ class AppInstaller < RailsInstaller
7
+ application_name 'my_shiny_metal_app'
8
+ support_location 'our shiny website'
9
+ rails_version '1.1.4'
10
+ end
11
+
12
+ # Installer program
13
+ directory = ARGV[1]
14
+
15
+ app = AppInstaller.new(directory)
16
+ app.message_proc = Proc.new do |msg|
17
+ STDERR.puts " #{msg}"
18
+ end
19
+ app.execute_command(*ARGV)
@@ -0,0 +1,5 @@
1
+ ---
2
+ rails-environment: production
3
+ database: sqlite
4
+ web-server: mongrel
5
+ threads: 2
data/examples/typo ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rails-installer'
4
+
5
+ class TypoInstaller < RailsInstaller
6
+ application_name 'typo'
7
+ support_location 'the Typo mailing list'
8
+ rails_version '1.1.4'
9
+
10
+ def install_post_hook
11
+ sweep_cache
12
+ end
13
+
14
+ # Sweep the cache
15
+ def sweep_cache
16
+ Dir.chdir(install_directory)
17
+ message "Cleaning out #{@@app_name.capitalize}'s cache"
18
+ status = system("rake -s sweep_cache > /dev/null 2> /dev/null")
19
+ end
20
+ end
21
+
22
+ class SweepCache < RailsInstaller::Command
23
+ help "Sweep Typo's cache"
24
+
25
+ def self.command(installer, *args)
26
+ installer.sweep_cache
27
+ end
28
+ end
29
+
30
+ # Installer program
31
+ directory = ARGV[1]
32
+
33
+ typo = TypoInstaller.new(directory)
34
+ typo.message_proc = Proc.new do |msg|
35
+ STDERR.puts " #{msg}"
36
+ end
37
+ typo.execute_command(*ARGV)
@@ -0,0 +1,33 @@
1
+ # Apache 1.3 HTTP proxy example for Typo with Mongrel
2
+ #
3
+ # Before any of this will work, you need to make sure that the mod_proxy
4
+ # modules are loaded. For shared hosting, your provider will have do this
5
+ # globally. The line required looks like this, although the exact path may
6
+ # vary:
7
+ #
8
+ # LoadModule proxy_module /usr/lib/apache/modules/mod_proxy.so
9
+ #
10
+ # Then you'll want a VirtualHost section that looks about like this:
11
+
12
+ <VirtualHost blog.example.com>
13
+ ServerName blog.example.com
14
+ ServerAlias www.blog.example.com
15
+
16
+ # Change this to your email address
17
+ ServerAdmin webmaster@localhost
18
+
19
+ # Change these to be valid paths for your host. The DocumentRoot path
20
+ # isn't very important because we don't actually use it for anything.
21
+ # For security's sake, it's best that it points to an empty directory,
22
+ # but that's not critical.
23
+ DocumentRoot /var/www/blog
24
+ ErrorLog /var/log/apache2/blog_error.log
25
+ CustomLog /var/log/apache2/blog_access.log combined
26
+
27
+ ServerSignature On
28
+
29
+ # This is the important part--it sets up proxying.
30
+ ProxyRequests Off
31
+ ProxyPass / $RAILS_URL
32
+ ProxyPassReverse / $RAILS_URL
33
+ </VirtualHost>
@@ -0,0 +1,40 @@
1
+ # Apache 2.0/2.2 HTTP proxy example for Typo with Mongrel
2
+ #
3
+ # Before any of this will work, you need to make sure that the mod_proxy and
4
+ # mod_proxy_http modules are loaded. For shared hosting, your provider will
5
+ # have do this globally. The two lines required look like this, although the
6
+ # exact path may vary:
7
+ #
8
+ # LoadModule proxy_module /usr/lib/apache2/modules/mod_proxy.so
9
+ # LoadModule proxy_http_module /usr/lib/apache2/modules/mod_proxy_http.so
10
+ #
11
+ # Then you'll want a VirtualHost section that looks about like this:
12
+
13
+ <VirtualHost blog.example.com>
14
+ ServerName blog.example.com
15
+ ServerAlias www.blog.example.com
16
+
17
+ # Change this to your email address
18
+ ServerAdmin webmaster@localhost
19
+
20
+ # Change these to be valid paths for your host. The DocumentRoot path
21
+ # isn't very important because we don't actually use it for anything.
22
+ # For security's sake, it's best that it points to an empty directory,
23
+ # but that's not critical.
24
+ DocumentRoot /var/www/blog
25
+ ErrorLog /var/log/apache2/blog_error.log
26
+ CustomLog /var/log/apache2/blog_access.log combined
27
+
28
+ ServerSignature On
29
+
30
+ # This is the important part--it sets up proxying.
31
+ ProxyRequests Off
32
+ <Proxy *>
33
+ Order deny,allow
34
+ Allow from all
35
+ </Proxy>
36
+
37
+ ProxyPass / $RAILS_URL
38
+ ProxyPassReverse / $RAILS_URL
39
+ ProxyPreserveHost On
40
+ </VirtualHost>
@@ -0,0 +1,6 @@
1
+ # This is untested and incomplete.
2
+ # See http://mongrel.rubyforge.org/docs/lighttpd.html
3
+ #
4
+ proxy.balance = "fair"
5
+ proxy.server = ( "/" =>
6
+ ( ( "host" => "$RAILS_HOST", "port" => $RAILS_PORT ) ) )
@@ -0,0 +1,575 @@
1
+ require 'rubygems'
2
+
3
+ require 'fileutils'
4
+ require 'yaml'
5
+ require 'digest/sha1'
6
+
7
+ require 'rails-installer/databases'
8
+ require 'rails-installer/web-servers'
9
+ require 'rails-installer/commands'
10
+
11
+ #
12
+ # An installer for Rails applications.
13
+ #
14
+ # The Rails Application Installer is designed to make it easy for end-users to
15
+ # install open-source Rails apps with a minimum amount of effort. When built
16
+ # properly, all the user needs to do is run:
17
+ #
18
+ # $ gem install my_app
19
+ # $ my_app install /some/path
20
+ #
21
+ # To use this installer, you'll need to create a small driver program (the
22
+ # 'my_app' program from above). Here's a minimal example:
23
+ #
24
+ # #!/usr/bin/env ruby
25
+ #
26
+ # require 'rubygems'
27
+ # require 'rails-installer'
28
+ #
29
+ # class AppInstaller < RailsInstaller
30
+ # application_name 'my_shiny_metal_app'
31
+ # support_location 'our shiny website'
32
+ # rails_version '1.1.4'
33
+ # end
34
+ #
35
+ # # Installer program
36
+ # directory = ARGV[1]
37
+ #
38
+ # app = AppInstaller.new(directory)
39
+ # app.message_proc = Proc.new do |msg|
40
+ # STDERR.puts " #{msg}"
41
+ # end
42
+ # app.execute_command(*ARGV)
43
+ #
44
+ # Place this in your application's gem/ directory, and then add it to your
45
+ # .gem using the 'executables' gemspec option. See the examples/ directory for
46
+ # more complex examples.
47
+ class RailsInstaller
48
+ include FileUtils
49
+ attr_accessor :install_directory, :source_directory, :config
50
+ attr_accessor :message_proc
51
+
52
+ class InstallFailed < StandardError; end
53
+
54
+ @@rails_version = nil
55
+
56
+ # The application name. Set this in your derived class.
57
+ def self.application_name(name)
58
+ @@app_name = name
59
+ end
60
+
61
+ # The support location. This is displayed to the user at the end of the
62
+ # install process.
63
+ def self.support_location(location)
64
+ @@support_location = location
65
+ end
66
+
67
+ # Which Rails version this app needs. This version of Rails will be frozen
68
+ # into vendor/rails/
69
+ def self.rails_version(svn_tag)
70
+ @@rails_version = svn_tag
71
+ end
72
+
73
+ # The application name, as set by +application_name+.
74
+ def app_name
75
+ @@app_name
76
+ end
77
+
78
+ def initialize(install_directory)
79
+ # use an absolute path, not a relative path.
80
+ if install_directory
81
+ @install_directory = File.expand_path(install_directory)
82
+ end
83
+
84
+ @config = read_yml(config_file) rescue nil
85
+ @config ||= Hash.new
86
+ end
87
+
88
+ # Display a status message
89
+ def message(string)
90
+ if message_proc
91
+ message_proc.call(string)
92
+ else
93
+ STDERR.puts string
94
+ end
95
+ end
96
+
97
+ # Install Application
98
+ def install(version=nil)
99
+ @source_directory = find_source_directory(@@app_name,version)
100
+
101
+ # Merge default configuration settings
102
+ @config = read_yml(backup_config_file).merge(config)
103
+
104
+ install_sequence
105
+
106
+ message ''
107
+ message "#{@@app_name.capitalize} is now running on http://#{`hostname`.chomp}:#{config['port-number']}"
108
+ message "Use '#{@@app_name} start #{install_directory}' to restart after boot."
109
+ message "Look in installer/*.conf.example to see how to integrate with your web server."
110
+ end
111
+
112
+ # The default install sequence. Override this if you need to add extra
113
+ # steps to the installer.
114
+ def install_sequence
115
+ stop
116
+
117
+ backup_database
118
+ install_pre_hook
119
+ pre_migrate_database
120
+ copy_files
121
+ freeze_rails
122
+ create_default_config_files
123
+ fix_permissions
124
+ create_directories
125
+ create_initial_database
126
+ set_initial_port_number
127
+ expand_template_files
128
+
129
+ migrate
130
+ install_post_hook
131
+ save
132
+
133
+ run_rails_tests
134
+
135
+ start
136
+ end
137
+
138
+ # The easy way to add steps to the installation process. +install_pre_hook+
139
+ # runs right after the DB is backed up and right before the first migration
140
+ # attempt.
141
+ def install_pre_hook
142
+ end
143
+
144
+ # Another install hook; +install_post_hook+ runs after the final migration.
145
+ def install_post_hook
146
+ end
147
+
148
+ # Start application in the background
149
+ def start(foreground = false)
150
+ server_class = RailsInstaller::WebServer.servers[config['web-server']]
151
+ if not server_class
152
+ message "** warning: web-server #{config['web-server']} unknown. Use 'web-server=external' to disable."
153
+ end
154
+
155
+ server_class.start(self,foreground)
156
+ end
157
+
158
+ # Stop application
159
+ def stop
160
+ return unless File.directory?(install_directory)
161
+
162
+ server_class = RailsInstaller::WebServer.servers[config['web-server']]
163
+ if not server_class
164
+ message "** warning: web-server #{config['web-server']} unknown. Use 'web-server=external' to disable."
165
+ end
166
+
167
+ server_class.stop(self)
168
+ end
169
+
170
+ # Backup the database
171
+ def backup_database
172
+ db_class = RailsInstaller::Database.dbs[config['database']]
173
+ db_class.backup(self)
174
+ end
175
+
176
+ # Restore the database
177
+ def restore_database(filename)
178
+ db_class = RailsInstaller::Database.dbs[config['database']]
179
+ in_directory install_directory do
180
+ db_class.restore(self, filename)
181
+ end
182
+ end
183
+
184
+ # Copy files from the source directory to the target directory.
185
+ def copy_files
186
+ message "Checking for existing #{@@app_name.capitalize} install in #{install_directory}"
187
+ files_yml = File.join(install_directory,'installer','files.yml')
188
+ old_files = read_yml(files_yml) rescue Hash.new
189
+
190
+ message "Reading files from #{source_directory}"
191
+ new_files = sha1_hash_directory_tree(source_directory)
192
+ new_files.delete('/config/database.yml') # Never copy this.
193
+
194
+ # Next, we compare the original install hash to the current hash. For each
195
+ # entry:
196
+ #
197
+ # - in new_file but not in old_files: copy
198
+ # - in old files but not in new_files: delete
199
+ # - in both, but hash different: copy
200
+ # - in both, hash same: don't copy
201
+ #
202
+ # We really should add a third hash (existing_files) and compare against that
203
+ # so we don't overwrite changed files.
204
+
205
+ added, changed, deleted, same = hash_diff(old_files, new_files)
206
+
207
+ if added.size > 0
208
+ message "Copying #{added.size} new files into #{install_directory}"
209
+ added.keys.sort.each do |file|
210
+ message " copying #{file}"
211
+ copy_one_file(file)
212
+ end
213
+ end
214
+
215
+ if changed.size > 0
216
+ message "Updating #{changed.size} files in #{install_directory}"
217
+ changed.keys.sort.each do |file|
218
+ message " updating #{file}"
219
+ copy_one_file(file)
220
+ end
221
+ end
222
+
223
+ if deleted.size > 0
224
+ message "Deleting #{deleted.size} files from #{install_directory}"
225
+
226
+ deleted.keys.sort.each do |file|
227
+ message " deleting #{file}"
228
+ rm(File.join(install_directory,file))
229
+ end
230
+ end
231
+
232
+ write_yml(files_yml,new_files)
233
+ end
234
+
235
+ # Copy one file from source_directory to install_directory, creating
236
+ # directories as needed.
237
+ def copy_one_file(filename)
238
+ source_name = File.join(source_directory,filename)
239
+ install_name = File.join(install_directory,filename)
240
+ dir_name = File.dirname(install_name)
241
+
242
+ mkdir_p(dir_name)
243
+ cp(source_name,install_name,:preserve => true)
244
+ end
245
+
246
+ # Compute the different between two hashes. Returns four hashes,
247
+ # one contains the keys that are in 'b' but not in 'a' (added entries),
248
+ # the next contains keys that are in 'a' and 'b', but have different values
249
+ # (changed). The third contains keys that are in 'b' but not 'a' (added).
250
+ # The final hash contains items that are the same in each.
251
+ def hash_diff(a, b)
252
+ added = {}
253
+ changed = {}
254
+ deleted = {}
255
+ same = {}
256
+
257
+ seen = {}
258
+
259
+ a.each_key do |k|
260
+ seen[k] = true
261
+
262
+ if b.has_key? k
263
+ if b[k] == a[k]
264
+ same[k] = true
265
+ else
266
+ changed[k] = true
267
+ end
268
+ else
269
+ deleted[k] = true
270
+ end
271
+ end
272
+
273
+ b.each_key do |k|
274
+ unless seen[k]
275
+ added[k] = true
276
+ end
277
+ end
278
+
279
+ [added, changed, deleted, same]
280
+ end
281
+
282
+ # Freeze to a specific version of Rails gems.
283
+ def freeze_rails
284
+ return unless @@rails_version
285
+ version_file = File.join(install_directory,'vendor','rails-version')
286
+ vendor_rails = File.join(install_directory,'vendor','rails')
287
+
288
+ old_version = File.read(version_file).chomp rescue nil
289
+
290
+ if @@rails_version == old_version
291
+ return
292
+ elsif old_version != nil
293
+ rm_rf(vendor_rails)
294
+ end
295
+
296
+ mkdir_p(vendor_rails)
297
+
298
+ package_map = {
299
+ 'rails' => File.join(vendor_rails,'railties'),
300
+ 'actionmailer' => File.join(vendor_rails,'actionmailer'),
301
+ 'actionpack' => File.join(vendor_rails,'actionpack'),
302
+ 'actionwebservice' => File.join(vendor_rails,'actionwebservice'),
303
+ 'activerecord' => File.join(vendor_rails,'activerecord'),
304
+ 'activesupport' => File.join(vendor_rails,'activesupport'),
305
+ }
306
+
307
+ specs = Gem.source_index.find_name('rails',["= #{@@rails_version}"])
308
+
309
+ unless specs.to_a.size > 0
310
+ raise InstallFailed, "Can't locate Rails #{@@rails_version}!"
311
+ end
312
+
313
+ copy_gem(specs.first, package_map[specs.first.name])
314
+
315
+ specs.first.dependencies.each do |dep|
316
+ next unless package_map[dep.name]
317
+
318
+ dep_spec = Gem.source_index.find_name(dep.name,[dep.version_requirements.to_s])
319
+ if dep_spec.size == 0
320
+ raise InstallFailed, "Can't locate dependency #{dep.name} #{dep.version_requirements.to_s}"
321
+ end
322
+
323
+ copy_gem(dep_spec.first, package_map[dep.name])
324
+ end
325
+
326
+ File.open(version_file,'w') do |f|
327
+ f.puts @@rails_version
328
+ end
329
+ end
330
+
331
+ # Copy a specific gem's contents.
332
+ def copy_gem(spec, destination)
333
+ message("copying #{spec.name} #{spec.version} to #{destination}")
334
+ cp_r("#{spec.full_gem_path}/.",destination)
335
+ end
336
+
337
+ # Create all default config files
338
+ def create_default_config_files
339
+ create_default_database_yml
340
+ end
341
+
342
+ # Create the default database.yml
343
+ def create_default_database_yml
344
+ db_class = RailsInstaller::Database.dbs[config['database']]
345
+ db_class.database_yml(self)
346
+ end
347
+
348
+ # Clean up file and directory permissions.
349
+ def fix_permissions
350
+ unless RUBY_PLATFORM =~ /mswin32/
351
+ message "Making scripts executable"
352
+ chmod 0555, File.join(install_directory,'public','dispatch.fcgi')
353
+ chmod 0555, File.join(install_directory,'public','dispatch.cgi')
354
+ chmod 0555, Dir[File.join(install_directory,'script','*')]
355
+ end
356
+ end
357
+
358
+ # Create required directories, like tmp
359
+ def create_directories
360
+ mkdir_p(File.join(install_directory,'tmp','cache'))
361
+ chmod(0755, File.join(install_directory,'tmp','cache'))
362
+ mkdir_p(File.join(install_directory,'tmp','session'))
363
+ mkdir_p(File.join(install_directory,'tmp','sockets'))
364
+ mkdir_p(File.join(install_directory,'log'))
365
+ File.open(File.join(install_directory,'log','development.log'),'w')
366
+ File.open(File.join(install_directory,'log','production.log'),'w')
367
+ File.open(File.join(install_directory,'log','testing.log'),'w')
368
+ end
369
+
370
+ # Create the initial SQLite database
371
+ def create_initial_database
372
+ db_class = RailsInstaller::Database.dbs[config['database']]
373
+ in_directory(install_directory) do
374
+ db_class.create(self)
375
+ end
376
+ end
377
+
378
+ # Get the current schema version
379
+ def get_schema_version
380
+ File.read(File.join(install_directory,'db','schema_version')).to_i rescue 0
381
+ end
382
+
383
+ # The path to the installed config file
384
+ def config_file
385
+ File.join(install_directory,'installer','rails_installer.yml')
386
+ end
387
+
388
+ # The path to the config file that comes with the GEM
389
+ def backup_config_file
390
+ File.join(source_directory,'installer','rails_installer_defaults.yml')
391
+ end
392
+
393
+ # Pick a default port number
394
+ def set_initial_port_number
395
+ config['port-number'] ||= (rand(1000)+4000)
396
+ end
397
+
398
+ # Pre-migrate the database. This checks to see if we're downgrading to an
399
+ # earlier version of our app, and runs 'rake migrate VERSION=...' to
400
+ # downgrade the database.
401
+ def pre_migrate_database
402
+ old_schema_version = get_schema_version
403
+ new_schema_version = File.read(File.join(source_directory,'db','schema_version')).to_i
404
+
405
+ return unless old_schema_version > 0
406
+
407
+ # Are we downgrading?
408
+ if old_schema_version > new_schema_version
409
+ message "Downgrading schema from #{old_schema_version} to #{new_schema_version}"
410
+
411
+ in_directory install_directory do
412
+ unless system("rake -s migrate VERSION=#{new_schema_version}")
413
+ raise InstallFailed, "Downgrade migrating from #{old_schema_version} to #{new_schema_version} failed."
414
+ end
415
+ end
416
+ end
417
+ end
418
+
419
+ # Migrate the database
420
+ def migrate
421
+ message "Migrating #{@@app_name.capitalize}'s database to newest release"
422
+
423
+ in_directory install_directory do
424
+ unless system("rake -s migrate")
425
+ raise InstallFailed, "Migration failed"
426
+ end
427
+ end
428
+ end
429
+
430
+ # Run Rails tests. This helps verify that we have a clean install
431
+ # with all dependencies. This cuts down on a lot of bug reports.
432
+ def run_rails_tests
433
+ message "Running tests. This may take a minute or two"
434
+
435
+ in_directory install_directory do
436
+ if system_silently("rake -s test")
437
+ message "All tests pass. Congratulations."
438
+ else
439
+ message "***** Tests failed *****"
440
+ message "** Please run 'rake test' by hand in your install directory."
441
+ message "** Report problems to #{@@support_location}."
442
+ message "***** Tests failed *****"
443
+ end
444
+ end
445
+ end
446
+
447
+ # Find all files in a directory tree and return a Hash containing sha1
448
+ # hashes of all files.
449
+ def sha1_hash_directory_tree(directory, prefix='', hash={})
450
+ Dir.entries(directory).each do |file|
451
+ next if file =~ /^\./
452
+ pathname = File.join(directory,file)
453
+ if File.directory?(pathname)
454
+ sha1_hash_directory_tree(pathname, File.join(prefix,file), hash)
455
+ else
456
+ hash[File.join(prefix,file)] = Digest::SHA1.hexdigest(File.read(pathname))
457
+ end
458
+ end
459
+
460
+ hash
461
+ end
462
+
463
+ # Save config settings
464
+ def save
465
+ write_yml(config_file,@config)
466
+ end
467
+
468
+ # Load a yaml file
469
+ def read_yml(filename)
470
+ YAML.load(File.read(filename))
471
+ end
472
+
473
+ # Save a yaml file.
474
+ def write_yml(filename,object)
475
+ File.open(filename,'w') do |f|
476
+ f.write(YAML.dump(object))
477
+ end
478
+ end
479
+
480
+ # Locate the source directory for a specific Version
481
+ def find_source_directory(gem_name, version)
482
+ if version == 'cwd'
483
+ return Dir.pwd
484
+ elsif version
485
+ version_array = ["= #{version}"]
486
+ else
487
+ version_array = ["> 0.0.0"]
488
+ end
489
+
490
+ specs = Gem.source_index.find_name(gem_name,version_array)
491
+ unless specs.to_a.size > 0
492
+ raise InstallFailed, "Can't locate version #{version}!"
493
+ end
494
+
495
+ specs.last.full_gem_path
496
+ end
497
+
498
+ # Call +system+, ignoring all output.
499
+ def system_silently(command)
500
+ if RUBY_PLATFORM =~ /mswin32/
501
+ null = 'NUL:'
502
+ else
503
+ null = '/dev/null'
504
+ end
505
+
506
+ system("#{command} > #{null} 2> #{null}")
507
+ end
508
+
509
+ # Expand configuration template files.
510
+ def expand_template_files
511
+ rails_host = config['bind-address'] || `hostname`.chomp
512
+ rails_port = config['port-number'].to_s
513
+ rails_url = "http://#{rails_host}:#{rails_port}"
514
+ Dir[File.join(install_directory,'installer','*.template')].each do |template_file|
515
+ output_file = template_file.gsub(/\.template/,'')
516
+ next if File.exists?(output_file) # don't overwrite files
517
+
518
+ message "expanding #{File.basename(output_file)} template"
519
+
520
+ text = File.read(template_file).gsub(/\$RAILS_URL/,rails_url).gsub(/\$RAILS_HOST/,rails_host).gsub(/\$RAILS_PORT/,rails_port)
521
+
522
+ File.open(output_file,'w') do |f|
523
+ f.write text
524
+ end
525
+ end
526
+ end
527
+
528
+ # Execute a command-line command
529
+ def execute_command(*args)
530
+ if args.size < 2
531
+ display_help
532
+ exit(1)
533
+ end
534
+
535
+ command_class = Command.commands[args.first]
536
+
537
+ if command_class
538
+ command_class.command(self,*(args[2..-1]))
539
+ else
540
+ display_help
541
+ exit(1)
542
+ end
543
+ end
544
+
545
+ # Display help.
546
+ def display_help(error=nil)
547
+ STDERR.puts error if error
548
+
549
+ commands = Command.commands.keys.sort
550
+ commands.each do |cmd|
551
+ cmd_class = Command.commands[cmd]
552
+ flag_help = cmd_class.flag_help_text.gsub(/APPNAME/,app_name)
553
+ help = cmd_class.help_text.gsub(/APPNAME/,app_name)
554
+
555
+ STDERR.puts " #{app_name} #{cmd} DIRECTORY #{flag_help}"
556
+ STDERR.puts " #{help}"
557
+ end
558
+ end
559
+ end
560
+
561
+ # Run a block inside of a specific directory. Chdir into the directory
562
+ # before executing the block, then chdir back to the original directory
563
+ # when the block exits.
564
+ def in_directory(directory)
565
+ begin
566
+ old_dir = Dir.pwd
567
+ Dir.chdir(directory)
568
+ value = yield
569
+ ensure
570
+ Dir.chdir(old_dir)
571
+ end
572
+
573
+ return value
574
+ end
575
+
@@ -0,0 +1,187 @@
1
+ class RailsInstaller
2
+
3
+ # Parent class for command-line subcommand plugins for the installer. Each
4
+ # subclass must implement the +command+ class method and should provide help
5
+ # text using the +help+ method. Example (from Typo):
6
+ #
7
+ # class SweepCache < RailsInstaller::Command
8
+ # help "Sweep Typo's cache"
9
+ #
10
+ # def self.command(installer, *args)
11
+ # installer.sweep_cache
12
+ # end
13
+ # end
14
+ #
15
+ # This implements a +sweep_cache+ command that Typo users can access by
16
+ # running 'typo sweep_cache /some/path'.
17
+ #
18
+ # Subcommands that need arguments should use both 'help' and 'flag_help',
19
+ # and then use the +args+ parameter to find their arguments. For example,
20
+ # the +install+ subcommand looks like this:
21
+ #
22
+ # class Install < RailsInstaller::Command
23
+ # help "Install or upgrade APPNAME in PATH."
24
+ # flag_help "[VERSION] [KEY=VALUE]..."
25
+ #
26
+ # def self.command(installer, *args)
27
+ # version = nil
28
+ # args.each do |arg|
29
+ # ...
30
+ # end
31
+ # end
32
+ # end
33
+ #
34
+ class Command
35
+ @@command_map = {}
36
+
37
+ # The +command+ method implements this sub-command. It's called by the
38
+ # command-line parser when the user asks for this sub-command.
39
+ def self.command(installer, *args)
40
+ raise "Not Implemented"
41
+ end
42
+
43
+ # +flag_help+ sets the help text for any arguments that this sub-command
44
+ # accepts. It defaults to ''.
45
+ def self.flag_help(text)
46
+ @flag_help = text
47
+ end
48
+
49
+ # Return the flag help text.
50
+ def self.flag_help_text
51
+ @flag_help || ''
52
+ end
53
+
54
+ # +help+ sets the help text for this subcommand.
55
+ def self.help(text)
56
+ @help = text
57
+ end
58
+
59
+ # Return the help text.
60
+ def self.help_text
61
+ @help || ''
62
+ end
63
+
64
+ def self.inherited(sub)
65
+ name = sub.to_s.gsub(/^.*::/,'').gsub(/([A-Z])/) do |match|
66
+ "_#{match.downcase}"
67
+ end.gsub(/^_/,'')
68
+
69
+ @@command_map[name] = sub
70
+ end
71
+
72
+ def self.commands
73
+ @@command_map
74
+ end
75
+
76
+ # The +install+ command installs the application into a specific path.
77
+ # Optionally, the user can request a specific version to install. If
78
+ # the version string is 'cwd', then the current directory is used as a
79
+ # template; otherwise it looks for the specified version number in the
80
+ # local Gems repository.
81
+ class Install < RailsInstaller::Command
82
+ help "Install or upgrade APPNAME in PATH."
83
+ flag_help "[VERSION] [KEY=VALUE]..."
84
+
85
+ def self.command(installer, *args)
86
+ version = nil
87
+ args.each do |arg|
88
+ if(arg =~ /^([^=]+)=(.*)$/)
89
+ installer.config[$1.to_s] = $2.to_s
90
+ else
91
+ version = arg
92
+ end
93
+ end
94
+
95
+ installer.install(version)
96
+ end
97
+ end
98
+
99
+ # The +config+ command controls the installation's config
100
+ # parameters. Running 'installer config /some/path' will show
101
+ # all of the config parameters for the installation in /some/path.
102
+ # You can set params with 'key=value', or clear them with 'key='.
103
+ class Config < RailsInstaller::Command
104
+ help "Read or set a configuration variable"
105
+ flag_help '[KEY=VALUE]...'
106
+
107
+ def self.command(installer, *args)
108
+ if args.size == 0
109
+ installer.config.keys.sort.each do |k|
110
+ puts "#{k}=#{installer.config[k]}"
111
+ end
112
+ else
113
+ args.each do |arg|
114
+ if(arg=~/^([^=]+)=(.*)$/)
115
+ if $2.to_s.empty?
116
+ installer.config.delete($1.to_s)
117
+ else
118
+ installer.config[$1.to_s]=$2.to_s
119
+ end
120
+ else
121
+ puts installer.config[arg]
122
+ end
123
+ end
124
+ installer.save
125
+ end
126
+ end
127
+ end
128
+
129
+ # The +start+ command starts a web server in the background for the
130
+ # specified installation, if applicable.
131
+ class Start < RailsInstaller::Command
132
+ help "Start the web server in the background"
133
+
134
+ def self.command(installer, *args)
135
+ installer.start
136
+ end
137
+ end
138
+
139
+ # The +run+ command starts a web server in the foreground.
140
+ class Run < RailsInstaller::Command
141
+ help "Start the web server in the foreground"
142
+
143
+ def self.command(installer, *args)
144
+ installer.start(true)
145
+ end
146
+ end
147
+
148
+ # The +restart+ command stops and restarts the web server.
149
+ class Restart < RailsInstaller::Command
150
+ help "Stop and restart the web server."
151
+
152
+ def self.command(installer, *args)
153
+ installer.stop
154
+ installer.start
155
+ end
156
+ end
157
+
158
+ # The +stop+ command shuts down the web server.
159
+ class Stop < RailsInstaller::Command
160
+ help "Stop the web server"
161
+
162
+ def self.command(installer, *args)
163
+ installer.stop
164
+ end
165
+ end
166
+
167
+ # The +backup+ command backs the database up into 'db/backups'.
168
+ class Backup < RailsInstaller::Command
169
+ help "Back up the database"
170
+
171
+ def self.command(installer, *args)
172
+ installer.backup_database
173
+ end
174
+ end
175
+
176
+ # The +restore+ command restores a backup.
177
+ class Restore < RailsInstaller::Command
178
+ help "Restore a database backup"
179
+ flag_help 'BACKUP_FILENAME'
180
+
181
+ def self.command(installer, *args)
182
+ installer.restore_database(args.first)
183
+ end
184
+ end
185
+ end
186
+ end
187
+
@@ -0,0 +1,222 @@
1
+ require 'active_record'
2
+
3
+ class RailsInstaller
4
+
5
+ # Parent class for database plugins for the installer. To create a new
6
+ # database handler, subclass this class and define a +yml+ class and
7
+ # optionally a +create_database+ method.
8
+ class Database
9
+ @@db_map = {}
10
+
11
+ # Connect to the database (using the 'database.yml' generated by the +yml+
12
+ # method). Returns true if the database already exists, and false if the
13
+ # database doesn't exist yet.
14
+ def self.connect(installer)
15
+ ActiveRecord::Base.establish_connection(
16
+ YAML.load(yml(installer))['production'])
17
+ begin
18
+ tables = ActiveRecord::Base.connection.tables
19
+ if tables.size > 0
20
+ return true
21
+ end
22
+ rescue
23
+ # okay
24
+ end
25
+ return false
26
+ end
27
+
28
+ # Back up the database. This is fully DB and schema agnostic. It
29
+ # serializes all tables to a single YAML file.
30
+ def self.backup(installer)
31
+ STDERR.puts "** backup **"
32
+ return unless connect(installer)
33
+
34
+ interesting_tables = ActiveRecord::Base.connection.tables.sort - ['sessions']
35
+ backup_dir = File.join(installer.install_directory, 'db', 'backup')
36
+ FileUtils.mkdir_p backup_dir
37
+ backup_file = File.join(backup_dir, "backup-#{Time.now.strftime('%Y%m%d-%H%M')}.yml")
38
+
39
+ installer.message "Backing up to #{backup_file}"
40
+
41
+ data = {}
42
+ interesting_tables.each do |tbl|
43
+ data[tbl] = ActiveRecord::Base.connection.select_all("select * from #{tbl}")
44
+ end
45
+
46
+ File.open(backup_file,'w') do |file|
47
+ YAML.dump data, file
48
+ end
49
+ end
50
+
51
+ # Restore a backup created by +backup+. Deletes all data before
52
+ # importing.
53
+ def self.restore(installer, filename)
54
+ connect(installer)
55
+ data = YAML.load(File.read(filename))
56
+
57
+ installer.message "Restoring data"
58
+ data.each_key do |table|
59
+ if table == 'schema_info'
60
+ ActiveRecord::Base.connection.execute("delete from schema_info")
61
+ ActiveRecord::Base.connection.execute("insert into schema_info (version) values (#{data[table].first['version']})")
62
+ else
63
+ installer.message " Restoring table #{table} (#{data[table].size})"
64
+
65
+ # Create a temporary model to talk to the DB
66
+ eval %Q{
67
+ class TempClass < ActiveRecord::Base
68
+ set_table_name '#{table}'
69
+ reset_column_information
70
+ end
71
+ }
72
+
73
+ TempClass.delete_all
74
+
75
+ data[table].each do |record|
76
+ r = TempClass.new(record)
77
+ r.save
78
+ end
79
+
80
+ if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!)
81
+ ActiveRecord::Base.connection.reset_pk_sequence!(table)
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ # Create a 'database.yml' file, using the data from +yml+.
88
+ def self.database_yml(installer)
89
+ yml_file = File.join(installer.install_directory,'config','database.yml')
90
+ return if File.exists? yml_file
91
+
92
+ File.open(yml_file,'w') do |f|
93
+ f.write(yml(installer))
94
+ end
95
+ end
96
+
97
+ # Create the database, including schema creation. This should be generic
98
+ # enough that database-specific drivers don't need to override it.
99
+ #
100
+ # It calls +create_database+ to actually build a new DB from scratch if
101
+ # needed.
102
+ def self.create(installer)
103
+ installer.message "Checking database"
104
+ if connect(installer)
105
+ installer.message "Database exists, preparing for upgrade"
106
+ return
107
+ end
108
+
109
+ installer.message "Creating initial database"
110
+
111
+ create_database(installer)
112
+
113
+ schema_file = File.join(installer.install_directory,'db',"schema.#{installer.config['database']}.sql")
114
+ schema = File.read(schema_file)
115
+
116
+ # Remove comments and extra blank lines
117
+ schema = schema.split(/\n/).map{|l| l.gsub(/^--.*/,'')}.select{|l| !(l=~/^$/)}.join("\n")
118
+
119
+ schema.split(/;\n/).each do |command|
120
+ ActiveRecord::Base.connection.execute(command)
121
+ end
122
+ end
123
+
124
+ # Create a new database from scratch. Some DBs, like SQLite, don't need
125
+ # this. Others will need to override this and call the DB's "create new
126
+ # database" command.
127
+ def self.create_database(installer)
128
+ # nothing
129
+ end
130
+
131
+ def self.inherited(sub)
132
+ name = sub.to_s.gsub(/^.*::/,'').gsub(/([A-Z])/) do |match|
133
+ "_#{match.downcase}"
134
+ end.gsub(/^_/,'')
135
+
136
+ @@db_map[name] = sub
137
+ end
138
+
139
+ def self.dbs
140
+ @@db_map
141
+ end
142
+
143
+ # The driver for SQLite 3. This is pretty minimal, as all we need is a
144
+ # +yml+ class to provide a basic 'database.yml'.
145
+ class Sqlite < RailsInstaller::Database
146
+ # The name of the sqlite database file
147
+ def self.db_file(installer)
148
+ File.join(installer.install_directory,'db','database.sqlite')
149
+ end
150
+
151
+ def self.yml(installer)
152
+ %q{
153
+ login: &login
154
+ adapter: sqlite3
155
+ database: db/database.sqlite
156
+
157
+ development:
158
+ <<: *login
159
+
160
+ production:
161
+ <<: *login
162
+
163
+ test:
164
+ database: ":memory:"
165
+ <<: *login
166
+ }
167
+ end
168
+ end
169
+
170
+ # A PostgreSQL driver. This is a bit more work then the SQLite driver, as
171
+ # Postgres needs to talk to its server. So it takes a number of config
172
+ # variables:
173
+ #
174
+ # * db_host
175
+ # * db_name
176
+ # * db_user
177
+ # * db_password
178
+ #
179
+ # It will call +createdb+ to set up the db all on its own.
180
+ class Postgresql < RailsInstaller::Database
181
+ def self.db_host(installer)
182
+ installer.config['db_host'] || 'localhost'
183
+ end
184
+
185
+ def self.db_user(installer)
186
+ installer.config['db_user'] || ENV['USER'] || installer.app_name
187
+ end
188
+
189
+ def self.db_name(installer)
190
+ installer.config['db_name'] || installer.app_name
191
+ end
192
+
193
+ def self.yml(installer)
194
+ %Q{
195
+ login: &login
196
+ adapter: postgresql
197
+ host: #{db_host installer}
198
+ username: #{db_user installer}
199
+ password: #{installer.config['db_password']}
200
+ database: #{db_name installer}
201
+
202
+ development:
203
+ <<: *login
204
+
205
+ production:
206
+ <<: *login
207
+
208
+ test:
209
+ database: #{db_name installer}-test
210
+ <<: *login
211
+ }
212
+ end
213
+
214
+ # Create a PostgreSQL database.
215
+ def self.create_database(installer)
216
+ installer.message "Creating PostgreSQL database"
217
+ system("createdb -U #{db_user installer} #{db_name installer}")
218
+ system("createdb -U #{db_user installer} #{db_name installer}-test")
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,115 @@
1
+ class RailsInstaller
2
+
3
+ # Parent class for webserver plugins for the installer. To create a new
4
+ # webserver handler, subclass this class and define a 'start' and 'stop'
5
+ # class method.
6
+ class WebServer
7
+ @@server_map = {}
8
+
9
+ # Start the server
10
+ def self.start(installer, foreground)
11
+ raise "Not Implemented"
12
+ end
13
+
14
+ # Stop the server
15
+ def self.stop(installer, foreground)
16
+ raise "Not Implemented"
17
+ end
18
+
19
+ def self.inherited(sub)
20
+ name = sub.to_s.gsub(/^.*::/,'').gsub(/([A-Z])/) do |match|
21
+ "_#{match.downcase}"
22
+ end.gsub(/^_/,'')
23
+
24
+ @@server_map[name] = sub
25
+ end
26
+
27
+ def self.servers
28
+ @@server_map
29
+ end
30
+
31
+ # A web server plugin for Mongrel (http://mongrel.rubyforge.org).
32
+ class Mongrel < RailsInstaller::WebServer
33
+ def self.start(installer, foreground)
34
+ args = {}
35
+ args['-p'] = installer.config['port-number']
36
+ args['-a'] = installer.config['bind-address']
37
+ args['-e'] = installer.config['rails-environment']
38
+ args['-d'] = foreground
39
+ args['-P'] = pid_file(installer)
40
+ args['--prefix'] = installer.config['url-prefix']
41
+
42
+ # Remove keys with nil values
43
+ args.delete_if {|k,v| v==nil}
44
+
45
+ args_array = args.to_a.flatten.map {|e| e.to_s}
46
+ args_array = ['mongrel_rails', 'start', installer.install_directory] + args_array
47
+ installer.message "Starting #{installer.app_name.capitalize} on port #{installer.config['port-number']}"
48
+ in_directory installer.install_directory do
49
+ system(args_array.join(' '))
50
+ end
51
+ end
52
+
53
+ def self.stop(installer)
54
+ args = {}
55
+ args['-P'] = pid_file(installer)
56
+
57
+ args_array = args.to_a.flatten.map {|e| e.to_s}
58
+ args_array = ['mongrel_rails', 'stop', installer.install_directory] + args_array
59
+ installer.message "Stopping #{installer.app_name.capitalize}"
60
+ in_directory installer.install_directory do
61
+ system(args_array.join(' '))
62
+ end
63
+
64
+ end
65
+
66
+ def self.pid_file(installer)
67
+ File.join(installer.install_directory,'tmp','pid.txt')
68
+ end
69
+ end
70
+
71
+ # A web server driver for MongrelCluster.
72
+ class MongrelCluster < RailsInstaller::WebServer
73
+ def self.start(installer, foreground)
74
+ args = {}
75
+ args['-p'] = installer.config['port-number']
76
+ args['-a'] = installer.config['bind-address']
77
+ args['-e'] = installer.config['rails-environment']
78
+ args['-N'] = installer.config['threads']
79
+ args['--prefix'] = installer.config['url-prefix']
80
+
81
+ # Remove keys with nil values
82
+ args.delete_if {|k,v| v==nil}
83
+
84
+ args_array = args.to_a.flatten.map {|e| e.to_s}
85
+ args_array = ['mongrel_rails', 'cluster::configure'] + args_array
86
+ installer.message "Configuring mongrel_cluster for #{installer.app_name.capitalize}"
87
+ in_directory installer.install_directory do
88
+ system(args_array.join(' '))
89
+ end
90
+ installer.message "Starting #{installer.app_name.capitalize} on port #{installer.config['port-number']}"
91
+ in_directory installer.install_directory do
92
+ system('mongrel_rails cluster::start')
93
+ end
94
+
95
+ end
96
+
97
+ def self.stop(installer)
98
+ installer.message "Stopping #{installer.app_name.capitalize}"
99
+ in_directory installer.install_directory do
100
+ system('mongrel_rails cluster::stop')
101
+ end
102
+ end
103
+ end
104
+
105
+ # Do-nothing webserver class. Used when the installer doesn't control the
106
+ # web server, like FastCGI.
107
+ class External < RailsInstaller::WebServer
108
+ def self.start(installer, foreground)
109
+ end
110
+
111
+ def self.stop(installer)
112
+ end
113
+ end
114
+ end
115
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.10
3
+ specification_version: 1
4
+ name: rails-app-installer
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.0
7
+ date: 2006-07-28
8
+ summary: An installer for Rails apps
9
+ require_paths:
10
+ - lib
11
+ email: scott@sigkill.org
12
+ homepage: http://scottstuff.net
13
+ rubyforge_project:
14
+ description: ''
15
+ autorequire: rails-installer
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ -
22
+ - ">"
23
+ - !ruby/object:Gem::Version
24
+ version: 0.0.0
25
+ version:
26
+ platform: ruby
27
+ authors:
28
+ - Scott Laird
29
+ files:
30
+ - lib/apache13.conf.example.template
31
+ - lib/apache20.conf.example.template
32
+ - lib/lighttpd.conf.example.template
33
+ - lib/rails-installer
34
+ - lib/rails-installer.rb
35
+ - examples/installer
36
+ - examples/rails_installer_defaults.yml
37
+ - examples/typo
38
+ - README
39
+ - lib/rails-installer/commands.rb
40
+ - lib/rails-installer/databases.rb
41
+ - lib/rails-installer/web-servers.rb
42
+ test_files: []
43
+ rdoc_options:
44
+ - "--main"
45
+ - RailsInstaller
46
+ extra_rdoc_files:
47
+ - README
48
+ - lib/rails-installer/commands.rb
49
+ - lib/rails-installer/databases.rb
50
+ - lib/rails-installer/web-servers.rb
51
+ executables: []
52
+ extensions: []
53
+ requirements: []
54
+ dependencies: []