# 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/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: Path to the Phusion Passenger support binary files. # - 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 specs = [ 'depcheck_specs/compiler_toolchain', 'depcheck_specs/ruby', 'depcheck_specs/gems', 'depcheck_specs/libs', 'depcheck_specs/utilities' ] ids = [ 'gcc', 'g++', 'gmake', 'download-tool', 'ruby-dev', 'ruby-openssl', 'rubygems', 'rake', 'rack', 'libcurl-dev', 'openssl-dev', 'zlib-dev', 'pcre-dev', 'daemon_controller >= 1.1.0' ] return [specs, ids] 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 -", "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) 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}'/*/{*.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 return 2 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 = "" if @targets.include?(:support_binaries) if ENV['PASSENGER_DEBUG'] && !ENV['PASSENGER_DEBUG'].empty? output_dir = "#{PhusionPassenger.source_root}/libout/common/libpassenger_common" else output_dir = "#{@support_dir}/libpassenger_common" end nginx_libs = COMMON_LIBRARY.only(*NGINX_LIBS_SELECTOR). set_output_dir(output_dir). link_objects_as_string command << "env PASSENGER_LIBS='#{nginx_libs} #{output_dir}/../libboost_oxt.a' " end # 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.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