# encoding: utf-8
#
# Phusion Passenger - http://www.modrails.com/
# Copyright (c) 2010, 2011, 2012 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/common_library'
require 'phusion_passenger/platform_info/ruby'
require 'phusion_passenger/platform_info/binary_compatibility'
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:
# - targets: An array containing at least one of:
# * :nginx - to indicate that you want to compile and install Nginx.
# * :support_binaries - to indicate that you want to compile and install the
# Phusion Passenger support binary files.
# * :ruby - to indicate that you want to compile and install the Ruby
# extension files.
#
# If 'targets' contains :nginx, then you must also specify these options:
# - nginx_dir: Nginx will be installed into this directory.
# - support_dir: See below.
# - nginx_version (optional): The Nginx version to download. If not given then a
# hardcoded version number will be used.
# - nginx_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 targets contains ':support_binaries', then you must also specify this
# options:
# - support_dir: The support binary files will be installed here.
#
# If targets contains ':ruby', then you must also specify this option:
# - ruby_dir: The support binary files will be installed here.
#
# Other optional options:
# - download_binaries: If true then RuntimeInstaller will attempt to download
# precompiled Nginx binaries and precompiled Phusion Passenger support binary
# 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::PCRE_Dev,
Dependencies::Daemon_Controller,
]
if Dependencies.fastthread_required?
result << Dependencies::FastThread
end
return result
end
def users_guide
return "#{PhusionPassenger.doc_dir}/Users guide Standalone.html"
end
def run_steps
if @support_dir && @nginx_dir
show_welcome_screen
end
check_dependencies(false) || exit(1)
puts
phase = 1
total_phases = 0
if binary_support_files_should_be_installed?
check_whether_we_can_write_to(@support_dir) || exit(1)
total_phases += 4
end
if ruby_extension_should_be_installed?
check_whether_we_can_write_to(@ruby_dir) || exit(1)
total_phases += 2
end
if nginx_needs_to_be_installed?
check_whether_we_can_write_to(@nginx_dir) || exit(1)
total_phases += 4
end
if binary_support_files_should_be_installed? && should_download_binaries?
download_and_extract_binary_support_files(@support_dir) do |progress, total|
show_progress(progress, total, 1, 1, "Extracting Passenger binaries...")
end
puts
puts
end
if ruby_extension_should_be_installed? && should_download_binaries?
download_and_extract_ruby_extension(@ruby_dir) do |progress, total|
show_progress(progress, total, 1, 1, "Extracting Ruby extension...")
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, phase, total_phases, "Extracting...")
end
phase += 1
if nginx_source_dir.nil?
puts
show_possible_solutions_for_download_and_extraction_problems
exit(1)
end
end
if ruby_extension_should_be_installed?
phase += install_ruby_extension do |progress, total, subphase, status_text|
show_progress(progress, total, phase + subphase, total_phases, status_text)
end
end
if binary_support_files_should_be_installed?
install_binary_support_files do |progress, total, subphase, status_text|
if subphase == 0
show_progress(progress, total, phase, total_phases, status_text)
else
show_progress(progress, total, phase + 1 .. phase + 3, total_phases, status_text)
end
end
phase += 4
end
if nginx_needs_to_be_installed?
install_nginx_from_source(nginx_source_dir) do |progress, total, status_text|
show_progress(progress, total, phase .. phase + 2, total_phases, status_text)
end
phase += 3
end
puts
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 @targets.include?(:nginx) &&
!File.exist?("#{@nginx_dir}/sbin/nginx")
end
def ruby_extension_should_be_installed?
return @targets.include?(:ruby) &&
!File.exist?("#{@ruby_dir}/#{PlatformInfo.ruby_extension_binary_compatibility_id}")
end
def binary_support_files_should_be_installed?
return @targets.include?(:support_binaries) && (
!File.exist?("#{@support_dir}/agents/PassengerHelperAgent") ||
!File.exist?("#{@support_dir}/libout/common/libpassenger_common.a")
)
end
def should_download_binaries?
return @download_binaries && @binaries_url_root
end
def show_welcome_screen
render_template 'standalone/welcome',
:version => @nginx_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
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 = ''
buffer = buffer.force_encoding('binary') if buffer.respond_to?(:force_encoding)
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}", :mode => File.stat(filename).mode)
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 STDERR_TO_STDOUT=1`.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_binary_support_files(target, &block)
puts "Downloading Passenger support binaries for your platform, if available..."
basename = "support-#{PlatformInfo.cxx_binary_compatibility_id}.tar.gz"
url = "#{@binaries_url_root}/#{PhusionPassenger::VERSION_STRING}/#{basename}"
tarball = "#{@working_dir}/#{basename}"
if !download(url, tarball)
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_ruby_extension(target, &block)
puts "Downloading Ruby extension for your Ruby and platform, if available..."
basename = "rubyext-#{PlatformInfo.ruby_extension_binary_compatibility_id}.tar.gz"
url = "#{@binaries_url_root}/#{PhusionPassenger::VERSION_STRING}/#{basename}"
tarball = "#{@working_dir}/#{basename}"
if !download(url, tarball)
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)
puts "Downloading Nginx binaries for your platform, if available..."
basename = "nginx-#{@nginx_version}-#{PlatformInfo.cxx_binary_compatibility_id}.tar.gz"
url = "#{@binaries_url_root}/#{PhusionPassenger::VERSION_STRING}/#{basename}"
tarball = "#{@working_dir}/#{basename}"
if !download(url, tarball)
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 @nginx_tarball
tarball = @nginx_tarball
else
puts "Downloading Nginx..."
basename = "nginx-#{@nginx_version}.tar.gz"
tarball = "#{@working_dir}/#{basename}"
if !download("http://nginx.org/download/#{basename}", tarball)
return nil
end
end
nginx_sources_name = "nginx-#{@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_ruby_extension
begin_progress_bar
yield(0, 1, 0, "Preparing Ruby extension...")
Dir.chdir(PhusionPassenger.source_root) do
run_rake_task!("native_support CACHING=false ONLY_RUBY=yes RUBY_EXTENSION_OUTPUT_DIR='#{@ruby_dir}'") do |progress, total|
yield(progress, total, 1, "Compiling Ruby extension...")
end
system "rm -rf '#{@ruby_dir}'/{*.log,*.o,Makefile}"
end
return 2
end
def install_binary_support_files
begin_progress_bar
yield(0, 1, 0, "Preparing Phusion Passenger...")
Dir.chdir(PhusionPassenger.source_root) do
args = "nginx_without_native_support" +
" CACHING=false" +
" OUTPUT_DIR='#{@support_dir}'" +
" AGENT_OUTPUT_DIR='#{@support_dir}'" +
" COMMON_OUTPUT_DIR='#{@support_dir}'" +
" LIBEV_OUTPUT_DIR='#{@support_dir}/libev'" +
" LIBEIO_OUTPUT_DIR='#{@support_dir}/libeio'"
run_rake_task!(args) do |progress, total|
yield(progress, total, 1, "Compiling Phusion Passenger...")
end
system "rm -rf '#{@support_dir}'/{*.o,*.dSYM,libboost_oxt}"
system "rm -rf '#{@support_dir}'/*/{*.o,*.lo,*.h,*.log,Makefile,libtool,stamp-h1,config.status,.deps}"
# Retain only the object files that are needed for linking the Phusion Passenger module into Nginx.
nginx_libs = COMMON_LIBRARY.
only(*NGINX_LIBS_SELECTOR).
set_output_dir("#{@support_dir}/libpassenger_common").
link_objects
(Dir["#{@support_dir}/libpassenger_common/**/*"] - nginx_libs).each do |filename|
if File.file?(filename)
File.unlink(filename)
end
end
end
return 2
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.
shell = PlatformInfo.find_command('bash') || "sh"
command = ""
if @targets.include?(:support_binaries)
nginx_libs = COMMON_LIBRARY.
only(*NGINX_LIBS_SELECTOR).
set_output_dir("#{PhusionPassenger.source_root}/libout/common/libpassenger_common").
link_objects_as_string
command << "env PASSENGER_LIBS='#{nginx_libs} #{@support_dir}/libboost_oxt.a' "
end
command << "#{shell} ./configure --prefix=/tmp " <<
"--with-cc-opt='-Wno-error' " <<
"--without-http_fastcgi_module " <<
"--without-http_scgi_module " <<
"--without-http_uwsgi_module " <<
"--with-http_gzip_static_module " <<
"'--add-module=#{PhusionPassenger.source_root}/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("cp -pR objs/nginx '#{@nginx_dir}/'")
STDERR.puts
STDERR.puts "*** ERROR: unable to copy Nginx binaries."
exit 1
end
if !system("strip '#{@nginx_dir}/nginx'")
STDERR.puts
STDERR.puts "*** ERROR: unable to strip debugging symbols from the Nginx binary."
exit 1
end
end
end
end
end # module Standalone
end # module PhusionPassenger