# encoding: utf-8
#
# Phusion Passenger - https://www.phusionpassenger.com/
# Copyright (c) 2010-2013 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/common_library'
require 'phusion_passenger/platform_info'
require 'phusion_passenger/platform_info/ruby'
require 'phusion_passenger/platform_info/binary_compatibility'
require 'phusion_passenger/standalone/utils'
require 'phusion_passenger/utils/tmpio'
module PhusionPassenger
module Standalone
# Installs the Phusion Passenger Standalone runtime by downloading or compiling
# the Phusion Passenger support binaries and Nginx, and then storing them
# in the designated directories. This installer is entirely non-interactive.
#
# The following option must be given:
# - targets: An array containing at least one of:
# * :support_binaries - to indicate that you want to install the
# Phusion Passenger support binary files.
# * :nginx - to indicate that you want to install Nginx.
#
# 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 `:nginx`, then you must also specify these options:
# - nginx_dir: Nginx will be installed into this directory.
# - lib_dir: Path to the Phusion Passenger libraries, which Nginx will link to.
# This may be the same path as `support_dir`; Nginx will be compiled
# after the support binary files are installed.
# - 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.
#
# Other optional options:
# - download_binaries: If true then RuntimeInstaller will attempt to download
# a precompiled Nginx binary and precompiled Phusion Passenger support binaries
# from the network, if they exist for the current platform. The default is
# true. Note that binary downloading only happens when Phusion Passenger is
# installed from an official release package.
# - binaries_url_root: The URL on which to look for the aforementioned binaries.
# The default points to the Phusion website.
class RuntimeInstaller < AbstractInstaller
include Utils
def initialize(*args)
super(*args)
raise ArgumentError, "At least one target must be given" if @targets.nil? || @targets.empty?
if @targets.include?(:support_binaries)
if PhusionPassenger.natively_packaged?
raise ArgumentError, "You cannot specify :support_binaries as a " +
"target when natively packaged"
end
raise ArgumentError, ":support_dir must be given" if !@support_dir
end
if @targets.include?(:nginx)
raise ArgumentError, ":nginx_dir must be given" if !@nginx_dir
raise ArgumentError, ":lib_dir must be given" if !@lib_dir
end
end
protected
def dependencies
specs = [
'depcheck_specs/compiler_toolchain',
'depcheck_specs/ruby',
'depcheck_specs/gems',
'depcheck_specs/libs',
'depcheck_specs/utilities'
]
ids = [
'gcc',
'g++',
'gmake',
'ruby-openssl',
'rubygems',
'rake',
'rack',
'libcurl-dev',
'openssl-dev',
'zlib-dev',
'pcre-dev',
'daemon_controller >= 1.1.0'
].compact
return [specs, ids]
end
def users_guide
return "#{PhusionPassenger.doc_dir}/Users guide Standalone.html"
end
def run_steps
show_welcome_screen if @nginx_dir
check_whether_os_is_broken
check_for_download_tool
download_or_compile_binaries
puts
puts "All done!"
puts
end
def before_install
super
@plugin.call_hook(:runtime_installer_start, self) if @plugin
@working_dir = PhusionPassenger::Utils.mktmpdir("passenger.", PlatformInfo.tmpexedir)
@nginx_version ||= PREFERRED_NGINX_VERSION
@download_binaries = true if !defined?(@download_binaries)
@binaries_url_root ||= BINARIES_URL_ROOT
end
def after_install
super
FileUtils.remove_entry_secure(@working_dir) if @working_dir
@plugin.call_hook(:runtime_installer_cleanup) if @plugin
end
private
def show_welcome_screen
render_template 'standalone/welcome',
:version => @nginx_version,
:dir => @nginx_dir
puts
end
def check_for_download_tool
puts "Checking for basic prerequities..."
puts
require 'phusion_passenger/platform_info/depcheck'
PlatformInfo::Depcheck.load('depcheck_specs/utilities')
runner = PlatformInfo::Depcheck::ConsoleRunner.new
runner.add('download-tool')
result = runner.check_all
puts
if !result
@download_binaries = false
line
puts
render_template 'standalone/download_tool_missing',
:runner => runner
wait
end
end
def download_or_compile_binaries
if should_install_support_binaries?
support_binaries_downloaded = download_support_binaries
end
if should_install_nginx?
nginx_binary_downloaded = download_nginx_binary
end
should_compile_support_binaries = should_install_support_binaries? &&
!support_binaries_downloaded
should_compile_nginx = should_install_nginx? && !nginx_binary_downloaded
if should_compile_support_binaries || should_compile_nginx
if @dont_compile_runtime
@stderr.puts "*** ERROR: Refusing to compile the Phusion Passenger Standalone runtime " +
"because --no-compile-runtime is given."
exit(1)
end
check_dependencies(false) || exit(1)
puts
if should_compile_support_binaries
check_whether_we_can_write_to(@support_dir) || exit(1)
end
if should_compile_nginx
check_whether_we_can_write_to(@nginx_dir) || exit(1)
end
end
if should_compile_nginx
nginx_source_dir = download_and_extract_nginx_sources
end
if should_compile_support_binaries
compile_support_binaries
end
if should_compile_nginx
compile_nginx(nginx_source_dir)
end
end
# If this method returns true, then PhusionPassenger.originally_packaged? is also true.
def should_install_support_binaries?
return @targets.include?(:support_binaries)
end
def should_install_nginx?
return @targets.include?(:nginx)
end
def should_download_binaries?
return PhusionPassenger.installed_from_release_package? &&
@download_binaries &&
@binaries_url_root
end
def download_support_binaries
return false if !should_download_binaries?
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, :cacert => PhusionPassenger.binaries_ca_cert_path, :use_cache => true)
puts "No binaries are available for your platform. But don't worry, the " +
"necessary binaries will be compiled from source instead."
puts
return false
end
FileUtils.mkdir_p(@support_dir)
Dir.mkdir("#{@working_dir}/support")
Dir.chdir("#{@working_dir}/support") do
puts "Extracting tarball..."
return false if !extract_tarball(tarball)
return false if !check_support_binaries
end
if system("mv '#{@working_dir}/support'/* '#{@support_dir}'/")
return true
else
@stderr.puts "Error: could not move extracted files to the support directory"
return false
end
rescue Interrupt
exit 2
end
def check_support_binaries
["PassengerWatchdog", "PassengerHelperAgent", "PassengerLoggingAgent"].each do |exe|
puts "Checking whether the downloaded #{exe} binary is usable..."
output = `env LD_BIND_NOW=1 DYLD_BIND_AT_LAUNCH=1 ./agents/#{exe} --test-binary 1`
if !$? || $?.exitstatus != 0 || output != "PASS\n"
@stderr.puts "Binary #{exe} is not usable."
return false
end
end
puts "All support binaries are usable."
return true
end
def download_nginx_binary
return false if !should_download_binaries?
puts "Downloading Nginx binary 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, :cacert => PhusionPassenger.binaries_ca_cert_path, :use_cache => true)
puts "No binary available for your platform. But don't worry, the " +
"necessary binary will be compiled from source instead."
puts
return false
end
FileUtils.mkdir_p(@nginx_dir)
Dir.mkdir("#{@working_dir}/nginx")
Dir.chdir("#{@working_dir}/nginx") do
puts "Extracting tarball..."
result = extract_tarball(tarball)
return false if !result
if check_nginx_binary
if system("mv '#{@working_dir}/nginx'/* '#{@nginx_dir}'/")
return true
else
@stderr.puts "Error: could not move extracted Nginx binary to the right directory"
return false
end
else
return false
end
end
rescue Interrupt
exit 2
end
def check_nginx_binary
puts "Checking whether the downloaded binary is usable..."
output = `env LD_BIND_NOW=1 DYLD_BIND_AT_LAUNCH=1 ./nginx -v 2>&1`
if $? && $?.exitstatus == 0 && output =~ /nginx version:/
puts "Nginx binary is usable."
return true
else
@stderr.puts "Nginx binary is not usable."
return false
end
end
def download_and_extract_nginx_sources
begin_progress_bar
puts "Downloading Nginx..."
if @nginx_tarball
tarball = @nginx_tarball
else
basename = "nginx-#{@nginx_version}.tar.gz"
tarball = "#{@working_dir}/#{basename}"
if !download("http://nginx.org/download/#{basename}", tarball)
puts
show_possible_solutions_for_download_and_extraction_problems
exit(1)
end
end
nginx_sources_name = "nginx-#{@nginx_version}"
Dir.chdir(@working_dir) do
begin_progress_bar
begin
result = extract_tarball(tarball) do |progress, total|
show_progress(progress / total * 0.1, 1.0, 1, 1, "Extracting Nginx sources...")
end
rescue Exception
puts
raise
end
if result
return "#{@working_dir}/#{nginx_sources_name}"
else
puts
show_possible_solutions_for_download_and_extraction_problems
exit(1)
end
end
rescue Interrupt
exit 2
end
def compile_support_binaries
begin_progress_bar
show_progress(0, 1, 1, 1, "Preparing Phusion Passenger...")
Dir.chdir(PhusionPassenger.source_root) do
args = "nginx_without_native_support" +
" CACHING=false" +
" OUTPUT_DIR='#{@support_dir}'"
begin
run_rake_task!(args) do |progress, total|
show_progress(progress, total, 1, 1, "Compiling Phusion Passenger...")
end
ensure
puts
end
system "rm -rf '#{@support_dir}'/agents/{*.o,*.dSYM}"
system "rm -rf '#{@support_dir}'/common/libboost_oxt"
system "rm -rf '#{@support_dir}'/*/{*.lo,*.h,*.log,Makefile,libtool,stamp-h1,config.status,.deps}"
system "rm -rf '#{@support_dir}'/{libeio,libev}/*.o"
# 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/**/*"].each do |filename|
if !nginx_libs.include?(filename) && File.file?(filename)
File.unlink(filename)
end
end
end
end
def compile_nginx(nginx_source_dir)
install_nginx_from_source(nginx_source_dir) do |progress, total, status_text|
show_progress(0.1 + progress / total.to_f * 0.9, 1.0, 1, 1, status_text)
end
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]
if @stdout.tty?
@stdout.write("#{text}\r")
@stdout.flush
else
if @last_status_text != status_text
@last_status_text = status_text
@stdout.write("[#{status_text.sub(/\.*$/, '')}]")
end
@stdout.write(".")
@stdout.flush
end
@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 -", "wb") 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) if block_given?
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) if block_given?
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 install_nginx_from_source(source_dir)
require 'phusion_passenger/platform_info/compiler'
Dir.chdir(source_dir) do
shell = PlatformInfo.find_command('bash') || "sh"
command = ""
lib_dir = "#{@lib_dir}/common/libpassenger_common"
nginx_libs = COMMON_LIBRARY.only(*NGINX_LIBS_SELECTOR).
set_output_dir(lib_dir).
link_objects_as_string
command << "env PASSENGER_INCLUDEDIR='#{PhusionPassenger.include_dir}' " <<
"PASSENGER_LIBS='#{nginx_libs} #{lib_dir}/../libboost_oxt.a' "
# 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 << "#{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 " <<
"--with-http_stub_status_module " <<
"'--add-module=#{PhusionPassenger.nginx_module_source_dir}'"
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 binary."
exit 1
end
if !strip_binary("#{@nginx_dir}/nginx")
@stderr.puts
@stderr.puts "*** ERROR: unable to strip debugging symbols from the Nginx binary."
exit 1
end
end
end
def strip_binary(filename)
return system("strip", filename)
end
end
end # module Standalone
end # module PhusionPassenger