# Phusion Passenger - http://www.modrails.com/ # Copyright (c) 2010 Phusion # # "Phusion Passenger" is a trademark of Hongli Lai & Ninh Bui. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'fileutils' require 'phusion_passenger' require 'phusion_passenger/abstract_installer' require 'phusion_passenger/packaging' require 'phusion_passenger/dependencies' require 'phusion_passenger/platform_info/ruby' require 'phusion_passenger/standalone/utils' module PhusionPassenger module Standalone # Installs the Phusion Passenger Standalone runtime by downloading and compiling # Nginx, compiling the Phusion Passenger support binaries, and storing the # results in the designated directories. This installer is entirely # non-interactive. # # The following option must be given: # - source_root: Path to the Phusion Passenger source root. # # If you want RuntimeInstaller to compile and install Nginx, then you must # specify these options: # - nginx_dir: Nginx will be installed into this directory. # - support_dir: See below. # - version (optional): The Nginx version to download. If not given then a # hardcoded version number will be used. # - tarball (optional): The location to the Nginx tarball. This tarball *must* # contain the Nginx version as specified by +version+. If +tarball+ is given # then Nginx will not be downloaded; it will be extracted from this tarball # instead. # # If you want RuntimeInstaller to compile and install the Phusion Passenger # support files, then you must specify these: # - support_dir: The support files will be installed here. Should not equal # +source_root+, or funny things might happen. # # Other optional options: # - download_binaries: If true then RuntimeInstaller will attempt to download # precompiled Nginx binaries and precompiled Phusion Passenger support files # from the network, if they exist for the current platform. The default is # false. # - binaries_url_root: The URL on which to look for the aforementioned binaries. # The default points to the Phusion website. # # Please note that RuntimeInstaller will try to avoid compiling/installing things # that don't need to be compiled/installed. This is done by checking whether some # key files exist, and concluding that something doesn't need to be # compiled/installed if they do. This quick check is of course not perfect; if you # want to force a recompilation/reinstall then you should remove +nginx_dir+ # and +support_dir+ before starting this installer. class RuntimeInstaller < AbstractInstaller include Utils protected def dependencies result = [ Dependencies::GCC, Dependencies::GnuMake, Dependencies::DownloadTool, Dependencies::Ruby_DevHeaders, Dependencies::Ruby_OpenSSL, Dependencies::RubyGems, Dependencies::Rake, Dependencies::Rack, Dependencies::Curl_Dev, Dependencies::OpenSSL_Dev, Dependencies::Zlib_Dev, Dependencies::File_Tail, Dependencies::Daemon_Controller, ] if Dependencies.fastthread_required? result << Dependencies::FastThread end if Dependencies.asciidoc_required? result << Dependencies::AsciiDoc end return result end def users_guide return "#{DOCDIR}/Users guide Standalone.html" end def install! if @support_dir && @nginx_dir show_welcome_screen end check_dependencies(false) || exit(1) puts if passenger_support_files_need_to_be_installed? check_whether_we_can_write_to(@support_dir) || exit(1) end if nginx_needs_to_be_installed? check_whether_we_can_write_to(@nginx_dir) || exit(1) end if passenger_support_files_need_to_be_installed? && should_download_binaries? download_and_extract_passenger_binaries(@support_dir) do |progress, total| show_progress(progress, total, 1, 1, "Extracting Passenger binaries...") end puts puts end if nginx_needs_to_be_installed? && should_download_binaries? download_and_extract_nginx_binaries(@nginx_dir) do |progress, total| show_progress(progress, total, 1, 1, "Extracting Nginx binaries...") end puts puts end if nginx_needs_to_be_installed? nginx_source_dir = download_and_extract_nginx_sources do |progress, total| show_progress(progress, total, 1, 7, "Extracting...") end if nginx_source_dir.nil? puts show_possible_solutions_for_download_and_extraction_problems exit(1) end end if passenger_support_files_need_to_be_installed? install_passenger_support_files do |progress, total, phase, status_text| if phase == 1 show_progress(progress, total, 2, 7, status_text) else show_progress(progress, total, 3..5, 7, status_text) end end end if nginx_needs_to_be_installed? install_nginx_from_source(nginx_source_dir) do |progress, total, status_text| show_progress(progress, total, 6..7, 7, status_text) end end puts color_puts "All done!" puts end def before_install super @plugin.call_hook(:runtime_installer_start, self) if @plugin @working_dir = "/tmp/#{myself}-passenger-standalone-#{Process.pid}" FileUtils.rm_rf(@working_dir) FileUtils.mkdir_p(@working_dir) @download_binaries = true if !defined?(@download_binaries) @binaries_url_root ||= STANDALONE_BINARIES_URL_ROOT end def after_install super FileUtils.rm_rf(@working_dir) @plugin.call_hook(:runtime_installer_cleanup) if @plugin end private def nginx_needs_to_be_installed? return @nginx_dir && !File.exist?("#{@nginx_dir}/sbin/nginx") end def passenger_support_files_need_to_be_installed? return @support_dir && !File.exist?("#{@support_dir}/Rakefile") end def should_download_binaries? return @download_binaries && @binaries_url_root end def show_welcome_screen render_template 'standalone/welcome', :version => @version, :dir => @nginx_dir puts end def check_whether_we_can_write_to(dir) FileUtils.mkdir_p(dir) File.new("#{dir}/__test__.txt", "w").close return true rescue new_screen if Process.uid == 0 render_template 'standalone/cannot_write_to_dir', :dir => dir else render_template 'standalone/run_installer_as_root', :dir => dir end return false ensure File.unlink("#{dir}/__test__.txt") rescue nil end def show_progress(progress, total, phase, total_phases, status_text = "") if !phase.is_a?(Range) phase = phase..phase end total_progress = (phase.first - 1).to_f / total_phases total_progress += (progress.to_f / total) * ((phase.last - phase.first + 1).to_f / total_phases) max_width = 79 progress_bar_width = 45 text = sprintf("[%-#{progress_bar_width}s] %s", '*' * (progress_bar_width * total_progress).to_i, status_text) text = text.ljust(max_width) text = text[0 .. max_width - 1] STDOUT.write("#{text}\r") STDOUT.flush @plugin.call_hook(:runtime_installer_progress, total_progress, status_text) if @plugin end def myself return `whoami`.strip end def begin_progress_bar if !@begun @begun = true color_puts "Installing Phusion Passenger Standalone..." end end def show_possible_solutions_for_download_and_extraction_problems new_screen render_template "standalone/possible_solutions_for_download_and_extraction_problems" puts end def extract_tarball(filename) File.open(filename, 'rb') do |f| IO.popen("tar xzf -", "w") do |io| buffer = '' total_size = File.size(filename) bytes_read = 0 yield(bytes_read, total_size) begin doing_our_io = true while !f.eof? f.read(1024 * 8, buffer) io.write(buffer) io.flush bytes_read += buffer.size doing_our_io = false yield(bytes_read, total_size) doing_our_io = true end rescue Errno::EPIPE if doing_our_io return false else raise end end end if $?.exitstatus != 0 return false end end return true end def run_command_with_throbber(command, status_text) backlog = "" IO.popen("#{command} 2>&1", "r") do |io| throbbers = ['-', '\\', '|', '/'] index = 0 while !io.eof? backlog << io.readline yield("#{status_text} #{throbbers[index]}") index = (index + 1) % throbbers.size end end if $?.exitstatus != 0 STDERR.puts STDERR.puts backlog STDERR.puts "*** ERROR: command failed: #{command}" exit 1 end end def copy_files(files, target) FileUtils.mkdir_p(target) files.each_with_index do |filename, i| next if File.directory?(filename) dir = "#{target}/#{File.dirname(filename)}" if !File.directory?(dir) FileUtils.mkdir_p(dir) end FileUtils.install(filename, "#{target}/#{filename}") yield(i + 1, files.size) end end def rake return PlatformInfo.rake_command end def run_rake_task!(target) total_lines = `#{rake} #{target} --dry-run`.split("\n").size - 1 backlog = "" IO.popen("#{rake} #{target} --trace STDERR_TO_STDOUT=1", "r") do |io| progress = 1 while !io.eof? line = io.readline if line =~ /^\*\* / yield(progress, total_lines) backlog.replace("") progress += 1 else backlog << line end end end if $?.exitstatus != 0 STDERR.puts STDERR.puts "*** ERROR: the following command failed:" STDERR.puts(backlog) exit 1 end end def download_and_extract_passenger_binaries(target, &block) color_puts "Downloading Passenger binaries for your platform, if available..." url = "#{@binaries_url_root}/#{runtime_version_string}/support.tar.gz" tarball = "#{@working_dir}/support.tar.gz" if !download(url, tarball) color_puts "Looks like it's not. But don't worry, the " + "necessary binaries will be compiled from source instead." return nil end FileUtils.mkdir_p(target) Dir.chdir(target) do return extract_tarball(tarball, &block) end rescue Interrupt exit 2 end def download_and_extract_nginx_binaries(target, &block) color_puts "Downloading Nginx binaries for your platform, if available..." basename = "nginx-#{@version}.tar.gz" url = "#{@binaries_url_root}/#{runtime_version_string}/#{basename}" tarball = "#{@working_dir}/#{basename}" if !download(url, tarball) color_puts "Looks like it's not. But don't worry, the " + "necessary binaries will be compiled from source instead." return nil end FileUtils.mkdir_p(target) Dir.chdir(target) do return extract_tarball(tarball, &block) end rescue Interrupt exit 2 end def download_and_extract_nginx_sources(&block) if @tarball tarball = @tarball else color_puts "Downloading Nginx..." basename = "nginx-#{@version}.tar.gz" tarball = "#{@working_dir}/#{basename}" if !download("http://sysoev.ru/nginx/#{basename}", tarball) return nil end end nginx_sources_name = "nginx-#{@version}" Dir.chdir(@working_dir) do begin_progress_bar if extract_tarball(tarball, &block) return "#{@working_dir}/#{nginx_sources_name}" else return nil end end rescue Interrupt exit 2 end def install_passenger_support_files begin_progress_bar # Copy Phusion Passenger sources to designated directory if necessary. yield(0, 1, 1, "Preparing Phusion Passenger...") FileUtils.rm_rf(@support_dir) Dir.chdir(@source_root) do files = `#{rake} package:filelist --silent`.split("\n") copy_files(files, @support_dir) do |progress, total| yield(progress, total, 1, "Copying files...") end end # Then compile it. yield(0, 1, 2, "Preparing Phusion Passenger...") Dir.chdir(@support_dir) do run_rake_task!("nginx RELEASE=yes") do |progress, total| yield(progress, total, 2, "Compiling Phusion Passenger...") end end end def install_nginx_from_source(source_dir) require 'phusion_passenger/platform_info/compiler' Dir.chdir(source_dir) do # RPM thinks it's being smart by scanning binaries for # paths and refusing to create package if it detects any # hardcoded thats that point to /usr or other important # locations. For Phusion Passenger Standalone we do not # care at all what the Nginx configured prefix is because # we pass it its resource locations during runtime, so # work around the problem by configure Nginx with prefix # /tmp. command = "sh ./configure --prefix=/tmp " << "--without-pcre " << "--without-http_rewrite_module " << "--without-http_fastcgi_module " << "'--add-module=#{@support_dir}/ext/nginx'" run_command_with_throbber(command, "Preparing Nginx...") do |status_text| yield(0, 1, status_text) end backlog = "" total_lines = `#{PlatformInfo.gnu_make} --dry-run`.split("\n").size IO.popen("#{PlatformInfo.gnu_make} 2>&1", "r") do |io| progress = 1 while !io.eof? line = io.readline backlog << line yield(progress, total_lines, "Compiling Nginx core...") progress += 1 end end if $?.exitstatus != 0 STDERR.puts STDERR.puts "*** ERROR: unable to compile Nginx." STDERR.puts backlog exit 1 end yield(1, 1, 'Copying files...') if !system("mkdir -p '#{@nginx_dir}/sbin'") || !system("cp -pR objs/nginx '#{@nginx_dir}/sbin/'") STDERR.puts STDERR.puts "*** ERROR: unable to copy Nginx binaries." exit 1 end end end end end # module Standalone end # module PhusionPassenger