Class: RailsInstaller

Inherits:
Object
  • Object
show all
Includes:
FileUtils
Defined in:
lib/rails-installer.rb,
lib/rails-installer/commands.rb,
lib/rails-installer/databases.rb,
lib/rails-installer/web-servers.rb

Overview

An installer for Rails applications.

The Rails Application Installer is designed to make it easy for end-users to install open-source Rails apps with a minimum amount of effort. When built properly, all the user needs to do is run:

$ gem install my_app
$ my_app install /some/path

To use this installer, you’ll need to create a small driver program (the ‘my_app’ program from above). Here’s a minimal example:

#!/usr/bin/env ruby

require 'rubygems'
require 'rails-installer'

class AppInstaller < RailsInstaller
  application_name 'my_shiny_metal_app'
  support_location 'our shiny website'
  rails_version '1.1.4'
end

# Installer program
directory = ARGV[1]

app = AppInstaller.new(directory)
app.message_proc = Proc.new do |msg|
  STDERR.puts " #{msg}"
end
app.execute_command(*ARGV)

Place this in your application’s gem/ directory, and then add it to your .gem using the ‘executables’ gemspec option. See the examples/ directory for more complex examples.

Defined Under Namespace

Classes: Command, Database, InstallFailed, WebServer

Constant Summary collapse

@@rails_version =
nil

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(install_directory) ⇒ RailsInstaller

Returns a new instance of RailsInstaller.



78
79
80
81
82
83
84
85
86
# File 'lib/rails-installer.rb', line 78

def initialize(install_directory)
  # use an absolute path, not a relative path.
  if install_directory
    @install_directory = File.expand_path(install_directory)
  end
      
  @config = read_yml(config_file) rescue nil
  @config ||= Hash.new
end

Instance Attribute Details

#configObject

Returns the value of attribute config.



49
50
51
# File 'lib/rails-installer.rb', line 49

def config
  @config
end

#install_directoryObject

Returns the value of attribute install_directory.



49
50
51
# File 'lib/rails-installer.rb', line 49

def install_directory
  @install_directory
end

#message_procObject

Returns the value of attribute message_proc.



50
51
52
# File 'lib/rails-installer.rb', line 50

def message_proc
  @message_proc
end

#source_directoryObject

Returns the value of attribute source_directory.



49
50
51
# File 'lib/rails-installer.rb', line 49

def source_directory
  @source_directory
end

Class Method Details

.application_name(name) ⇒ Object

The application name. Set this in your derived class.



57
58
59
# File 'lib/rails-installer.rb', line 57

def self.application_name(name)
  @@app_name = name
end

.rails_version(svn_tag) ⇒ Object

Which Rails version this app needs. This version of Rails will be frozen into vendor/rails/



69
70
71
# File 'lib/rails-installer.rb', line 69

def self.rails_version(svn_tag)
  @@rails_version = svn_tag
end

.support_location(location) ⇒ Object

The support location. This is displayed to the user at the end of the install process.



63
64
65
# File 'lib/rails-installer.rb', line 63

def self.support_location(location)
  @@support_location = location
end

Instance Method Details

#app_nameObject

The application name, as set by application_name.



74
75
76
# File 'lib/rails-installer.rb', line 74

def app_name
  @@app_name
end

#backup_config_fileObject

The path to the config file that comes with the GEM



389
390
391
# File 'lib/rails-installer.rb', line 389

def backup_config_file
  File.join(source_directory,'installer','rails_installer_defaults.yml')
end

#backup_databaseObject

Backup the database



171
172
173
174
# File 'lib/rails-installer.rb', line 171

def backup_database
  db_class = RailsInstaller::Database.dbs[config['database']]
  db_class.backup(self)
end

#config_fileObject

The path to the installed config file



384
385
386
# File 'lib/rails-installer.rb', line 384

def config_file
  File.join(install_directory,'installer','rails_installer.yml')
end

#copy_filesObject

Copy files from the source directory to the target directory.



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/rails-installer.rb', line 185

def copy_files
  message "Checking for existing #{@@app_name.capitalize} install in #{install_directory}"
  files_yml = File.join(install_directory,'installer','files.yml')
  old_files = read_yml(files_yml) rescue Hash.new
  
  message "Reading files from #{source_directory}"
  new_files = sha1_hash_directory_tree(source_directory)
  new_files.delete('/config/database.yml') # Never copy this.
  
  # Next, we compare the original install hash to the current hash.  For each
  # entry:
  #
  # - in new_file but not in old_files: copy
  # - in old files but not in new_files: delete
  # - in both, but hash different: copy
  # - in both, hash same: don't copy
  #
  # We really should add a third hash (existing_files) and compare against that
  # so we don't overwrite changed files.

  added, changed, deleted, same = hash_diff(old_files, new_files)
  
  if added.size > 0
    message "Copying #{added.size} new files into #{install_directory}"
    added.keys.sort.each do |file|
      message " copying #{file}"
      copy_one_file(file)
    end
  end
  
  if changed.size > 0
    message "Updating #{changed.size} files in #{install_directory}"
    changed.keys.sort.each do |file|
      message " updating #{file}"
      copy_one_file(file)
    end
  end
  
  if deleted.size > 0
    message "Deleting #{deleted.size} files from #{install_directory}"
    
    deleted.keys.sort.each do |file|
      message " deleting #{file}"
      rm(File.join(install_directory,file))
    end
  end
  
  write_yml(files_yml,new_files)
end

#copy_gem(spec, destination) ⇒ Object

Copy a specific gem’s contents.



332
333
334
335
# File 'lib/rails-installer.rb', line 332

def copy_gem(spec, destination)
  message("copying #{spec.name} #{spec.version} to #{destination}")
  cp_r("#{spec.full_gem_path}/.",destination)
end

#copy_one_file(filename) ⇒ Object

Copy one file from source_directory to install_directory, creating directories as needed.



237
238
239
240
241
242
243
244
# File 'lib/rails-installer.rb', line 237

def copy_one_file(filename)
  source_name = File.join(source_directory,filename)
  install_name = File.join(install_directory,filename)
  dir_name = File.dirname(install_name)
  
  mkdir_p(dir_name)
  cp(source_name,install_name,:preserve => true)
end

#create_default_config_filesObject

Create all default config files



338
339
340
# File 'lib/rails-installer.rb', line 338

def create_default_config_files
  create_default_database_yml
end

#create_default_database_ymlObject

Create the default database.yml



343
344
345
346
# File 'lib/rails-installer.rb', line 343

def create_default_database_yml
  db_class = RailsInstaller::Database.dbs[config['database']]
  db_class.database_yml(self)
end

#create_directoriesObject

Create required directories, like tmp



359
360
361
362
363
364
365
366
367
368
# File 'lib/rails-installer.rb', line 359

def create_directories
  mkdir_p(File.join(install_directory,'tmp','cache'))
  chmod(0755, File.join(install_directory,'tmp','cache'))
  mkdir_p(File.join(install_directory,'tmp','session'))
  mkdir_p(File.join(install_directory,'tmp','sockets'))
  mkdir_p(File.join(install_directory,'log'))
  File.open(File.join(install_directory,'log','development.log'),'w')
  File.open(File.join(install_directory,'log','production.log'),'w')
  File.open(File.join(install_directory,'log','testing.log'),'w')
end

#create_initial_databaseObject

Create the initial SQLite database



371
372
373
374
375
376
# File 'lib/rails-installer.rb', line 371

def create_initial_database
  db_class = RailsInstaller::Database.dbs[config['database']]
  in_directory(install_directory) do
    db_class.create(self)
  end
end

#display_help(error = nil) ⇒ Object

Display help.



546
547
548
549
550
551
552
553
554
555
556
557
558
# File 'lib/rails-installer.rb', line 546

def display_help(error=nil)
  STDERR.puts error if error
  
  commands = Command.commands.keys.sort
  commands.each do |cmd|
    cmd_class = Command.commands[cmd]
    flag_help = cmd_class.flag_help_text.gsub(/APPNAME/,app_name)
    help = cmd_class.help_text.gsub(/APPNAME/,app_name)
    
    STDERR.puts "  #{app_name} #{cmd} DIRECTORY #{flag_help}"
    STDERR.puts "    #{help}"
  end
end

#execute_command(*args) ⇒ Object

Execute a command-line command



529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
# File 'lib/rails-installer.rb', line 529

def execute_command(*args)
  if args.size < 2
    display_help
    exit(1)
  end
  
  command_class = Command.commands[args.first]
  
  if command_class
    command_class.command(self,*(args[2..-1]))
  else
    display_help
    exit(1)
  end
end

#expand_template_filesObject

Expand configuration template files.



510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
# File 'lib/rails-installer.rb', line 510

def expand_template_files
  rails_host = config['bind-address'] || `hostname`.chomp
  rails_port = config['port-number'].to_s
  rails_url = "http://#{rails_host}:#{rails_port}"
  Dir[File.join(install_directory,'installer','*.template')].each do |template_file|
    output_file = template_file.gsub(/\.template/,'')
    next if File.exists?(output_file) # don't overwrite files

    message "expanding #{File.basename(output_file)} template"
    
    text = File.read(template_file).gsub(/\$RAILS_URL/,rails_url).gsub(/\$RAILS_HOST/,rails_host).gsub(/\$RAILS_PORT/,rails_port)
    
    File.open(output_file,'w') do |f|
      f.write text
    end
  end
end

#find_source_directory(gem_name, version) ⇒ Object

Locate the source directory for a specific Version



481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
# File 'lib/rails-installer.rb', line 481

def find_source_directory(gem_name, version)
  if version == 'cwd'
    return Dir.pwd
  elsif version
    version_array = ["= #{version}"]
  else
    version_array = ["> 0.0.0"]
  end
  
  specs = Gem.source_index.find_name(gem_name,version_array)
  unless specs.to_a.size > 0
    raise InstallFailed, "Can't locate version #{version}!"
  end
  
  specs.last.full_gem_path
end

#fix_permissionsObject

Clean up file and directory permissions.



349
350
351
352
353
354
355
356
# File 'lib/rails-installer.rb', line 349

def fix_permissions
  unless RUBY_PLATFORM =~ /mswin32/
    message "Making scripts executable"
    chmod 0555, File.join(install_directory,'public','dispatch.fcgi')
    chmod 0555, File.join(install_directory,'public','dispatch.cgi')
    chmod 0555, Dir[File.join(install_directory,'script','*')]
  end
end

#freeze_railsObject

Freeze to a specific version of Rails gems.



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/rails-installer.rb', line 283

def freeze_rails
  return unless @@rails_version
  version_file = File.join(install_directory,'vendor','rails-version')
  vendor_rails = File.join(install_directory,'vendor','rails')
  
  old_version = File.read(version_file).chomp rescue nil
  
  if @@rails_version == old_version
    return
  elsif old_version != nil
    rm_rf(vendor_rails)
  end

  mkdir_p(vendor_rails)
  
  package_map = {
    'rails' => File.join(vendor_rails,'railties'),
    'actionmailer' => File.join(vendor_rails,'actionmailer'),
    'actionpack' => File.join(vendor_rails,'actionpack'),
    'actionwebservice' => File.join(vendor_rails,'actionwebservice'),
    'activerecord' => File.join(vendor_rails,'activerecord'),
    'activesupport' => File.join(vendor_rails,'activesupport'),
  }
  
  specs = Gem.source_index.find_name('rails',["= #{@@rails_version}"])
  
  unless specs.to_a.size > 0
    raise InstallFailed, "Can't locate Rails #{@@rails_version}!"
  end
  
  copy_gem(specs.first, package_map[specs.first.name])
  
  specs.first.dependencies.each do |dep|
    next unless package_map[dep.name]
    
    dep_spec = Gem.source_index.find_name(dep.name,[dep.version_requirements.to_s])
    if dep_spec.size == 0
      raise InstallFailed, "Can't locate dependency #{dep.name} #{dep.version_requirements.to_s}"
    end
    
    copy_gem(dep_spec.first, package_map[dep.name])
  end
  
  File.open(version_file,'w') do |f|
    f.puts @@rails_version
  end
end

#get_schema_versionObject

Get the current schema version



379
380
381
# File 'lib/rails-installer.rb', line 379

def get_schema_version
  File.read(File.join(install_directory,'db','schema_version')).to_i rescue 0
end

#hash_diff(a, b) ⇒ Object

Compute the different between two hashes. Returns four hashes, one contains the keys that are in ‘b’ but not in ‘a’ (added entries), the next contains keys that are in ‘a’ and ‘b’, but have different values (changed). The third contains keys that are in ‘b’ but not ‘a’ (added). The final hash contains items that are the same in each.



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/rails-installer.rb', line 251

def hash_diff(a, b)
  added = {}
  changed = {}
  deleted = {}
  same = {}
  
  seen = {}
  
  a.each_key do |k|
    seen[k] = true
    
    if b.has_key? k
      if b[k] == a[k]
        same[k] = true
      else
        changed[k] = true
      end
    else
      deleted[k] = true
    end
  end
  
  b.each_key do |k|
    unless seen[k]
      added[k] = true
    end
  end
  
  [added, changed, deleted, same]
end

#install(version = nil) ⇒ Object

Install Application



98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/rails-installer.rb', line 98

def install(version=nil)
  @source_directory = find_source_directory(@@app_name,version)

  # Merge default configuration settings
  @config = read_yml(backup_config_file).merge(config)
  
  install_sequence
  
  message ''
  message "#{@@app_name.capitalize} is now running on http://#{`hostname`.chomp}:#{config['port-number']}"
  message "Use '#{@@app_name} start #{install_directory}' to restart after boot."
  message "Look in installer/*.conf.example to see how to integrate with your web server."
end

#install_post_hookObject

Another install hook; install_post_hook runs after the final migration.



145
146
# File 'lib/rails-installer.rb', line 145

def install_post_hook
end

#install_pre_hookObject

The easy way to add steps to the installation process. install_pre_hook runs right after the DB is backed up and right before the first migration attempt.



141
142
# File 'lib/rails-installer.rb', line 141

def install_pre_hook
end

#install_sequenceObject

The default install sequence. Override this if you need to add extra steps to the installer.



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/rails-installer.rb', line 114

def install_sequence
  stop
  
  backup_database
  install_pre_hook
  pre_migrate_database
  copy_files
  freeze_rails
  create_default_config_files
  fix_permissions
  create_directories
  create_initial_database
  set_initial_port_number
  expand_template_files
  
  migrate
  install_post_hook
  save
  
  run_rails_tests
  
  start
end

#message(string) ⇒ Object

Display a status message



89
90
91
92
93
94
95
# File 'lib/rails-installer.rb', line 89

def message(string)
  if message_proc
    message_proc.call(string)
  else
    STDERR.puts string
  end
end

#migrateObject

Migrate the database



420
421
422
423
424
425
426
427
428
# File 'lib/rails-installer.rb', line 420

def migrate
  message "Migrating #{@@app_name.capitalize}'s database to newest release"
  
  in_directory install_directory do
    unless system("rake -s migrate")
      raise InstallFailed, "Migration failed"
    end
  end
end

#pre_migrate_databaseObject

Pre-migrate the database. This checks to see if we’re downgrading to an earlier version of our app, and runs ‘rake migrate VERSION=…’ to downgrade the database.



401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
# File 'lib/rails-installer.rb', line 401

def pre_migrate_database
  old_schema_version = get_schema_version
  new_schema_version = File.read(File.join(source_directory,'db','schema_version')).to_i
  
  return unless old_schema_version > 0
   
  # Are we downgrading?
  if old_schema_version > new_schema_version
    message "Downgrading schema from #{old_schema_version} to #{new_schema_version}"
    
    in_directory install_directory do
      unless system("rake -s migrate VERSION=#{new_schema_version}")
        raise InstallFailed, "Downgrade migrating from #{old_schema_version} to #{new_schema_version} failed."
      end
    end
  end
end

#read_yml(filename) ⇒ Object

Load a yaml file



469
470
471
# File 'lib/rails-installer.rb', line 469

def read_yml(filename)
  YAML.load(File.read(filename))
end

#restore_database(filename) ⇒ Object

Restore the database



177
178
179
180
181
182
# File 'lib/rails-installer.rb', line 177

def restore_database(filename)
  db_class = RailsInstaller::Database.dbs[config['database']]
  in_directory install_directory do
    db_class.restore(self, filename)
  end
end

#run_rails_testsObject

Run Rails tests. This helps verify that we have a clean install with all dependencies. This cuts down on a lot of bug reports.



432
433
434
435
436
437
438
439
440
441
442
443
444
445
# File 'lib/rails-installer.rb', line 432

def run_rails_tests
  message "Running tests.  This may take a minute or two"
  
  in_directory install_directory do
    if system_silently("rake -s test")
      message "All tests pass.  Congratulations."
    else
      message "***** Tests failed *****"
      message "** Please run 'rake test' by hand in your install directory."
      message "** Report problems to #{@@support_location}."
      message "***** Tests failed *****"
    end
  end
end

#saveObject

Save config settings



464
465
466
# File 'lib/rails-installer.rb', line 464

def save
  write_yml(config_file,@config)
end

#set_initial_port_numberObject

Pick a default port number



394
395
396
# File 'lib/rails-installer.rb', line 394

def set_initial_port_number
  config['port-number'] ||= (rand(1000)+4000)
end

#sha1_hash_directory_tree(directory, prefix = '', hash = {}) ⇒ Object

Find all files in a directory tree and return a Hash containing sha1 hashes of all files.



449
450
451
452
453
454
455
456
457
458
459
460
461
# File 'lib/rails-installer.rb', line 449

def sha1_hash_directory_tree(directory, prefix='', hash={})
  Dir.entries(directory).each do |file|
    next if file =~ /^\./
    pathname = File.join(directory,file)
    if File.directory?(pathname)
      sha1_hash_directory_tree(pathname, File.join(prefix,file), hash)
    else
      hash[File.join(prefix,file)] = Digest::SHA1.hexdigest(File.read(pathname))
    end
  end
  
  hash
end

#start(foreground = false) ⇒ Object

Start application in the background



149
150
151
152
153
154
155
156
# File 'lib/rails-installer.rb', line 149

def start(foreground = false)
  server_class = RailsInstaller::WebServer.servers[config['web-server']]
  if not server_class
    message "** warning: web-server #{config['web-server']} unknown.  Use 'web-server=external' to disable."
  end
  
  server_class.start(self,foreground)
end

#stopObject

Stop application



159
160
161
162
163
164
165
166
167
168
# File 'lib/rails-installer.rb', line 159

def stop
  return unless File.directory?(install_directory)
  
  server_class = RailsInstaller::WebServer.servers[config['web-server']]
  if not server_class
    message "** warning: web-server #{config['web-server']} unknown.  Use 'web-server=external' to disable."
  end
  
  server_class.stop(self)
end

#system_silently(command) ⇒ Object

Call system, ignoring all output.



499
500
501
502
503
504
505
506
507
# File 'lib/rails-installer.rb', line 499

def system_silently(command)
  if RUBY_PLATFORM =~ /mswin32/
    null = 'NUL:'
  else
    null = '/dev/null'
  end
  
  system("#{command} > #{null} 2> #{null}")
end

#write_yml(filename, object) ⇒ Object

Save a yaml file.



474
475
476
477
478
# File 'lib/rails-installer.rb', line 474

def write_yml(filename,object)
  File.open(filename,'w') do |f|
    f.write(YAML.dump(object))
  end
end