rails-app-installer 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/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: []