lib/phusion_passenger/platform_info/depcheck.rb



# encoding: utf-8
require 'phusion_passenger/platform_info/ruby'
require 'phusion_passenger/platform_info/linux'
require 'phusion_passenger/platform_info/compiler'
require 'phusion_passenger/platform_info/operating_system'
require 'phusion_passenger/utils/ansi_colors'

module PhusionPassenger
module PlatformInfo

# Almost all software require other software in order to run. We call those
# other software 'dependencies'. Reliably checking for dependencies can be
# difficult. Helping the user in case a dependency is not installed (or
# doesn't seem to be installed) is more difficult still.
# 
# The Depcheck framework seeks to make all this easier. It allows the programmer
# to write "specs" which contain dependency checking code in a structured way.
# The programmer defines a dependency's basic information (name, website, etc),
# defines installation instructions (which may be customized per platform) and
# defines code for checking whether the dependency actually exists. The Depcheck
# framework:
# 
#  * Provides helpers for checking for the existance of commands, libraries,
#    headers, etc.
#  * Registers all dependency specs in a way that can be easily accessed
#    structurally.
#  * Allows user-friendly display of dependency checking progress and user help
#    instructions.
# 
# Most dependency checking code (e.g. autoconf) is very straightforward: they
# just check for the existance of a command, library, header, etc and either
# report "found" or "not found". In our experience the world is unfortunately
# not that simple. Users can have multiple versions of a dependency installed,
# where some dependencies are suitable while others are not. Therefore specs
# should print as many details about the dependency as possible (location, version,
# etc) so that the user can override any decisions if necessary.
module Depcheck
	THIS_DIR   = File.expand_path(File.dirname(__FILE__))
	@@loaded   = {}
	@@database = {}

	def self.load(partial_filename)
		if !@@loaded[partial_filename]
			filename = "#{THIS_DIR}/#{partial_filename}.rb"
			content = File.read(filename)
			instance_eval(content, filename)
			@@loaded[partial_filename] = true
		end
	end

	def self.define(identifier, &block)
		@@database[identifier.to_s] = block
	end

	def self.find(identifier)
		# We lazy-initialize everything in order to save resources. This also
		# allows blocks to perform relatively expensive checks without hindering
		# startup time.
		identifier = identifier.to_s
		result = @@database[identifier]
		if result.is_a?(Proc)
			result = Dependency.new(&result)
			@@database[identifier] = result
		end
		result
	end

	class Dependency
		def initialize(&block)
			instance_eval(&block)
			check_syntax_aspect("Name must be given") { !!@name }
			check_syntax_aspect("A checker must be given") { !!@checker }
		end

		def check
			@install_comments = nil
			@check_result ||= @checker.call
		end

		### DSL for specs ###

		def name(value = nil)
			value ? @name = value : @name
		end

		def website(value = nil)
			value ? @website = value : @website
		end

		def website_comments(value = nil)
			value ? @website_comments = value : @website_comments
		end

		def install_instructions(value = nil)
			if value
				@install_instructions = value
			else
				if @install_instructions
					@install_instructions
				elsif @website
					result = "Please download it from <b>#{@website}</b>"
					result << "\n(#{@website_comments})" if @website_comments
					result
				else
					"Search Google for '#{@name}'."
				end
			end
		end

		def install_comments(value = nil)
			value ? @install_comments = value : @install_comments
		end

	private
		def check_syntax_aspect(description)
			if !yield
				raise description
			end
		end

		### DSL for specs ###

		def define_checker(&block)
			@checker = block
		end

		def check_for_command(name, *args)
			result = find_command(name, *args)
			if result
				{ :found => true,
				  "Location" => result }
			else
				false
			end
		end

		def check_for_ruby_tool(name)
			result = locate_ruby_tool(name)
			if result
				{ :found => true,
				  "Location" => result }
			else
				false
			end
		end

		def check_for_header(header_name, language = :c, flags = nil)
			if result = PlatformInfo.find_header(header_name, language, flags)
				{ :found => true,
				  "Location" => result }
			else
				false
			end
		end

		# def check_for_library(name)
		# 	check_by_compiling("int main() { return 0; }", :cxx, nil, "-l#{name}")
		# end

		# def check_by_compiling(source, language = :c, cflags = nil, linkflags = nil)
		# 	case language
		# 	when :c
		# 		source_file		= "#{PlatformInfo.tmpexedir}/depcheck-#{Process.pid}-#{Thread.current.object_id}.c"
		# 		compiler			 = "gcc"
		# 		compiler_flags = ENV['CFLAGS']
		# 	when :cxx
		# 		source_file		= "#{PlatformInfo.tmpexedir}/depcheck-#{Process.pid}-#{Thread.current.object_id}.cpp"
		# 		compiler			 = "g++"
		# 		compiler_flags = "#{ENV['CFLAGS']} #{ENV['CXXFLAGS']}".strip
		# 	else
		# 		raise ArgumentError, "Unknown language '#{language}"
		# 	end
		
		# 	output_file = "#{PlatformInfo.tmpexedir}/depcheck-#{Process.pid}-#{Thread.current.object_id}"
		
		# 	begin
		# 		File.open(source_file, 'w') do |f|
		# 			f.puts(source)
		# 		end
			
		# 		if find_command(compiler)
		# 			command = "#{compiler} #{compiler_flags} #{cflags} " +
		# 				"#{source_file} -o #{output_file} #{linkflags}"
		# 			[!!system(command)]
		# 		else
		# 			[:unknown, "Cannot check: compiler '#{compiler}' not found."]
		# 		end
		# 	ensure
		# 		File.unlink(source_file) rescue nil
		# 		File.unlink(output_file) rescue nil
		# 	end
		# end

		def check_for_ruby_library(name)
			begin
				require(name)
				{ :found => true }
			rescue LoadError
				if defined?(Gem)
					false
				else
					begin
						require 'rubygems'
						require(name)
						{ :found => true }
					rescue LoadError
						false
					end
				end
			end
		end

		def on(platform)
			return if @on_invoked
			invoke = false
			if (linux_distro_tags || []).include?(platform)
				invoke = true
			else
				case platform
				when :linux
					invoke = true if PlatformInfo.os_name =~ /linux/
				when :freebsd
					invoke = true if PlatformInfo.os_name =~ /freebsd/
				when :macosx
					invoke = true if PlatformInfo.os_name == "macosx"
				when :solaris
					invoke = true if PlatformInfo.os_name =~ /solaris/
				when :other_platforms
					invoke = true
				end
			end
			if invoke
				yield
				@on_invoked = true
			end
		end

		def apt_get_install(package_name)
			install_instructions("Please install it with <b>apt-get install #{package_name}</b>")
		end

		def urpmi(package_name)
			install_instructions("Please install it with <b>urpmi #{package_name}</b>")
		end

		def yum_install(package_name)
			install_instructions("Please install it with <b>yum install #{package_name}</b>")
		end

		def emerge(package_name)
			install_instructions("Please install it with <b>emerge -av #{package_name}</b>")
		end

		def gem_install(package_name)
			install_instructions("Please make sure RubyGems is installed, then run " +
				"<b>#{gem_command || 'gem'} install #{package_name}</b>")
		end

		def xcode_install(component)
			install_instructions("Please install <b>Xcode</b>, then in Xcode go to " +
				"<b>Preferences -> Downloads -> Components</b> and install <b>#{component}</b>")
		end


		def ruby_command
			PlatformInfo.ruby_command
		end

		def gem_command
			PlatformInfo.gem_command
		end

		def find_command(command, *args)
			PlatformInfo.find_command(command, *args)
		end

		def linux_distro_tags
			PlatformInfo.linux_distro_tags
		end

		def locate_ruby_tool(name)
			PlatformInfo.locate_ruby_tool(name)
		end
	end # class Dependency

	class ConsoleRunner
		attr_reader :missing_dependencies

		def initialize
			@stdout = STDOUT
			@dep_identifiers = []
		end

		def add(identifier)
			@dep_identifiers << identifier
		end

		def check_all
			old_log_impl = PlatformInfo.log_implementation
			begin
				PlatformInfo.log_implementation = lambda do |message|
					message = PlatformInfo.send(:reindent, message, 10)
					message.sub!(/^          /, '')
					STDOUT.puts "       -> #{message}"
				end
				@missing_dependencies = []
				@dep_identifiers.each do |identifier|
					dep = Depcheck.find(identifier)
					raise "Cannot find depcheck spec #{identifier.inspect}" if !dep
					puts_header "Checking for #{dep.name}..."
					result = dep.check
					result = { :found => false } if !result

					if result[:found] && !result[:error]
						puts_detail "Found: <green>yes</green>"
					else
						if result[:error]
							puts_detail "Found: #{result[:found] ? "<yellow>yes, but there was an error</yellow>" : "<red>no</red>"}"
							puts_detail "Error: <red>#{result[:error]}</red>"
						else
							puts_detail "Found: #{result[:found] ? "<green>yes</green>" : "<red>no</red>"}"
						end
						@missing_dependencies << dep
					end

					result.each_pair do |key, value|
						if key.is_a?(String)
							puts_detail "#{key}: #{value}"
						end
					end
				end

				return @missing_dependencies.empty?
			ensure
				PlatformInfo.log_implementation = old_log_impl
			end
		end

		def print_installation_instructions_for_missing_dependencies
			@missing_dependencies.each do |dep|
				puts " * To install <yellow>#{dep.name}</yellow>:"
				puts "   #{dep.install_instructions}"
				if dep.install_comments
					puts "   #{dep.install_comments}"
				end
				puts
			end
		end

	private
		def puts(text = nil)
			if text
				@stdout.puts(Utils::AnsiColors.ansi_colorize(text))
			else
				@stdout.puts
			end
			@stdout.flush
		end

		def puts_header(text)
			puts " <b>* #{text}</b>"
		end

		def puts_detail(text)
			puts "      #{text}"
		end
	end
end # module Depcheck

end # module PlatformInfo
end # module PhusionPassenger