#  Phusion Passenger - https://www.phusionpassenger.com/
#  Copyright (c) 2010-2014 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.

PhusionPassenger.require_passenger_lib 'platform_info'
PhusionPassenger.require_passenger_lib 'platform_info/operating_system'

module PhusionPassenger

module PlatformInfo
private
	def self.detect_language_extension(language)
		case language
		when :c
			return "c"
		when :cxx
			return "cpp"
		else
			raise ArgumentError, "Unsupported language #{language.inspect}"
		end
	end
	private_class_method :detect_language_extension

	def self.detect_compiler_type_name(language)
		case language
		when :c
			return "C"
		when :cxx
			return "C++"
		else
			raise ArgumentError, "Unsupported language #{language.inspect}"
		end
	end
	private_class_method :detect_compiler_type_name

	def self.create_compiler_command(language, flags1, flags2, link = false)
		case language
		when :c
			result  = [cc, link ? ENV['EXTRA_PRE_LDFLAGS'] : nil,
				ENV['EXTRA_PRE_CFLAGS'], flags1, flags2, ENV['EXTRA_CFLAGS'],
				ENV['EXTRA_LDFLAGS']]
		when :cxx
			result  = [cxx, link ? ENV['EXTRA_PRE_LDFLAGS'] : nil,
				ENV['EXTRA_PRE_CXXFLAGS'], flags1, flags2, ENV['EXTRA_CXXFLAGS'],
				ENV['EXTRA_LDFLAGS']]
		else
			raise ArgumentError, "Unsupported language #{language.inspect}"
		end
		return result.compact.join(" ").strip
	end
	private_class_method :create_compiler_command

	def self.run_compiler(description, command, source_file, source, capture_output = false)
		if verbose?
			message = "#{description}\n" <<
				"Running: #{command}\n"
			if source.strip.empty?
				message << "Source file is empty."
			else
				message << "Source file contains:\n" <<
					"-------------------------\n" <<
					unindent(source) <<
					"\n-------------------------"
			end
			log(message)
		end
		if capture_output
			begin
				output = `#{command} 2>&1`
				result = $?.exitstatus == 0
			rescue SystemCallError => e
				result = nil
				exec_error_reason = e.message
			end
			log("Output:\n" <<
				"-------------------------\n" <<
				output.to_s <<
				"\n-------------------------")
		elsif verbose?
			result = system(command)
		else
			result = system("(#{command}) >/dev/null 2>/dev/null")
		end
		if result.nil?
			log("Command could not be executed! #{exec_error_reason}".strip)
			return false
		elsif result
			log("Check suceeded")
			if capture_output
				return { :result => true, :output => output }
			else
				return true
			end
		else
			log("Check failed with exit status #{$?.exitstatus}")
			if capture_output == :always
				return { :result => false, :output => output }
			else
				return false
			end
		end
	end
	private_class_method :run_compiler

	def self.cc_or_cxx_supports_feliminate_unused_debug?(language)
		ext = detect_language_extension(language)
		compiler_type_name = detect_compiler_type_name(language)
		create_temp_file("passenger-compile-check.#{ext}") do |filename, f|
			f.close
			begin
				command = create_compiler_command(language,
					"-c '#{filename}' -o '#{filename}.o'",
					'-feliminate-unused-debug-symbols -feliminate-unused-debug-types')
				result = run_compiler("Checking for #{compiler_type_name} compiler '-feliminate-unused-debug-{symbols,types}' support",
					command, filename, '', true)
				return result && result[:output].empty?
			ensure
				File.unlink("#{filename}.o") rescue nil
			end
		end
	end
	private_class_method :cc_or_cxx_supports_feliminate_unused_debug?

public
	def self.cc
		return string_env('CC', default_cc)
	end
	memoize :cc

	def self.cxx
		return string_env('CXX', default_cxx)
	end
	memoize :cxx

	def self.default_cc
		# On most platforms, we'll want to use the same compiler as what the rest
		# of the system uses, so that we generate compatible binaries. That's
		# most likely the 'cc' command. We used to use 'gcc' by default.
		#
		# See for example this issue with OS X Mavericks (10.9). They switched from
		# GCC to Clang as the default compiler. Since the Nginx by default uses 'cc'
		# as the compiler, we'll have to do that too. Otherwise we'll get C++ linker
		# errors because Nginx is compiled with Clang while Phusion Passenger is
		# compiled with GCC.
		# https://code.google.com/p/phusion-passenger/issues/detail?id=950
		if PlatformInfo.find_command('cc')
			return 'cc'
		else
			return 'gcc'
		end
	end

	def self.default_cxx
		if PlatformInfo.find_command('c++')
			return 'c++'
		else
			return 'g++'
		end
	end

	def self.cc_is_gcc?
		`#{cc} -v 2>&1` =~ /gcc version/
	end
	memoize :cc_is_gcc?

	def self.cxx_is_gcc?
		`#{cxx} -v 2>&1` =~ /gcc version/
	end
	memoize :cxx_is_gcc?

	def self.cc_is_clang?
		`#{cc} --version 2>&1` =~ /clang version/
	end
	memoize :cc_is_clang?

	def self.cxx_is_clang?
		`#{cxx} --version 2>&1` =~ /clang version/
	end
	memoize :cxx_is_clang?

	def self.cc_is_sun_studio?
		`#{cc} -V 2>&1` =~ /Sun C/ || `#{cc} -flags 2>&1` =~ /Sun C/
	end
	memoize :cc_is_sun_studio?

	def self.cxx_is_sun_studio?
		`#{cxx} -V 2>&1` =~ /Sun C/ || `#{cxx} -flags 2>&1` =~ /Sun C/
	end
	memoize :cxx_is_sun_studio?


	# Looks for the given C or C++ header. This works by invoking the compiler and
	# searching in the compiler's header search path. Returns its full filename,
	# or true if this function knows that the header exists but can't find it (e.g.
	# because the compiler cannot tell us what its header search path is).
	# Returns nil if the header cannot be found.
	def self.find_header(header_name, language, flags = nil)
		extension = detect_language_extension(language)
		create_temp_file("passenger-compile-check.#{extension}") do |filename, f|
			source = %Q{
				#include <#{header_name}>
			}
			f.puts(source)
			f.close
			begin
				command = create_compiler_command(language,
					"-v -c '#{filename}' -o '#{filename}.o'",
					flags)
				if result = run_compiler("Checking for #{header_name}", command, filename, source, true)
					result[:output] =~ /^#include <...> search starts here:$(.+?)^End of search list\.$/m
					search_paths = $1.to_s.strip.split("\n").map{ |line| line.strip }
					search_paths.each do |dir|
						if File.file?("#{dir}/#{header_name}")
							return "#{dir}/#{header_name}"
						end
					end
					return true
				else
					return nil
				end
			ensure
				File.unlink("#{filename}.o") rescue nil
			end
		end
	end

	def self.try_compile(description, language, source, flags = nil)
		extension = detect_language_extension(language)
		create_temp_file("passenger-compile-check.#{extension}") do |filename, f|
			f.puts(source)
			f.close
			command = create_compiler_command(language,
				"-c '#{filename}' -o '#{filename}.o'",
				flags)
			return run_compiler(description, command, filename, source)
		end
	end

	# Like try_compile, but designed for checking whether a warning flag is
	# supported. Compilers sometimes do not error out upon encountering an
	# unsupported warning flag, but merely print a warning. This method checks
	# for that too.
	def self.try_compile_with_warning_flag(description, language, source, flags = nil)
		extension = detect_language_extension(language)
		create_temp_file("passenger-compile-check.#{extension}") do |filename, f|
			f.puts(source)
			f.close
			command = create_compiler_command(language,
				"-c '#{filename}' -o '#{filename}.o'",
				flags)
			result = run_compiler(description, command, filename, source, true)
			return result && result[:result] && result[:output] !~ /unknown warning option/i
		end
	end

	def self.try_link(description, language, source, flags = nil)
		extension = detect_language_extension(language)
		create_temp_file("passenger-link-check.#{extension}") do |filename, f|
			f.puts(source)
			f.close
			command = create_compiler_command(language,
				"'#{filename}' -o '#{filename}.out'",
				flags, true)
			return run_compiler(description, command, filename, source)
		end
	end

	def self.try_compile_and_run(description, language, source, flags = nil)
		extension = detect_language_extension(language)
		create_temp_file("passenger-run-check.#{extension}", tmpexedir) do |filename, f|
			f.puts(source)
			f.close
			command = create_compiler_command(language,
				"'#{filename}' -o '#{filename}.out'",
				flags, true)
			if run_compiler(description, command, filename, source)
				log("Running #{filename}.out")
				begin
					output = `'#{filename}.out' 2>&1`
				rescue SystemCallError => e
					log("Command failed: #{e}")
					return false
				end
				status = $?.exitstatus
				log("Command exited with status #{status}. Output:\n--------------\n#{output}\n--------------")
				return status == 0
			else
				return false
			end
		end
	end


	# Checks whether the compiler supports "-arch #{arch}".
	def self.compiler_supports_architecture?(arch)
		return try_compile("Checking for C compiler '-arch' support",
			:c, '', "-arch #{arch}")
	end

	def self.cc_supports_visibility_flag?
		return false if os_name =~ /aix/
		return try_compile("Checking for C compiler '-fvisibility' support",
			:c, '', '-fvisibility=hidden')
	end
	memoize :cc_supports_visibility_flag?, true

	def self.cxx_supports_visibility_flag?
		return false if os_name =~ /aix/
		return try_compile("Checking for C++ compiler '-fvisibility' support",
			:cxx, '', '-fvisibility=hidden')
	end
	memoize :cxx_supports_visibility_flag?, true

	def self.cc_supports_wno_attributes_flag?
		return try_compile_with_warning_flag(
			"Checking for C compiler '-Wno-attributes' support",
			:c, '', '-Wno-attributes')
	end
	memoize :cc_supports_wno_attributes_flag?, true

	def self.cxx_supports_wno_attributes_flag?
		return try_compile_with_warning_flag(
			"Checking for C++ compiler '-Wno-attributes' support",
			:cxx, '', '-Wno-attributes')
	end
	memoize :cxx_supports_wno_attributes_flag?, true

	def self.cc_supports_wno_missing_field_initializers_flag?
		return try_compile_with_warning_flag(
			"Checking for C compiler '-Wno-missing-field-initializers' support",
			:c, '', '-Wno-missing-field-initializers')
	end
	memoize :cc_supports_wno_missing_field_initializers_flag?, true

	def self.cxx_supports_wno_missing_field_initializers_flag?
		return try_compile_with_warning_flag(
			"Checking for C++ compiler '-Wno-missing-field-initializers' support",
			:cxx, '', '-Wno-missing-field-initializers')
	end
	memoize :cxx_supports_wno_missing_field_initializers_flag?, true

	def self.cxx_supports_wno_unused_local_typedefs_flag?
		return try_compile_with_warning_flag(
			"Checking for C++ compiler '-Wno-unused-local-typedefs' support",
			:cxx, '', '-Wno-unused-local-typedefs')
	end
	memoize :cxx_supports_wno_unused_local_typedefs_flag?, true

	def self.cc_supports_no_tls_direct_seg_refs_option?
		return try_compile("Checking for C compiler '-mno-tls-direct-seg-refs' support",
			:c, '', '-mno-tls-direct-seg-refs')
	end
	memoize :cc_supports_no_tls_direct_seg_refs_option?, true

	def self.cxx_supports_no_tls_direct_seg_refs_option?
		return try_compile("Checking for C++ compiler '-mno-tls-direct-seg-refs' support",
			:cxx, '', '-mno-tls-direct-seg-refs')
	end
	memoize :cxx_supports_no_tls_direct_seg_refs_option?, true

	def self.compiler_supports_wno_ambiguous_member_template?
		result = try_compile_with_warning_flag(
			"Checking for C++ compiler '-Wno-ambiguous-member-template' support",
			:cxx, '', '-Wno-ambiguous-member-template')
		return false if !result

		# For some reason, GCC does not complain about -Wno-ambiguous-member-template
		# not being supported unless the source contains another error. So we
		# check for this.
		create_temp_file("passenger-compile-check.cpp") do |filename, f|
			source = %Q{
				void foo() {
					return error;
				}
			}
			f.puts(source)
			f.close
			begin
				command = create_compiler_command(:cxx,
					"-c '#{filename}' -o '#{filename}.o'",
					'-Wno-ambiguous-member-template')
				result = run_compiler("Checking whether C++ compiler '-Wno-ambiguous-member-template' support is *really* supported",
					command, filename, source, :always)
			ensure
				File.unlink("#{filename}.o") rescue nil
			end
		end

		return result && result[:output] !~ /-Wno-ambiguous-member-template/
	end
	memoize :compiler_supports_wno_ambiguous_member_template?, true

	def self.cc_supports_feliminate_unused_debug?
		return cc_or_cxx_supports_feliminate_unused_debug?(:c)
	end
	memoize :cc_supports_feliminate_unused_debug?, true

	def self.cxx_supports_feliminate_unused_debug?
		return cc_or_cxx_supports_feliminate_unused_debug?(:cxx)
	end
	memoize :cxx_supports_feliminate_unused_debug?, true

	# Returns whether compiling C++ with -fvisibility=hidden might result
	# in tons of useless warnings, like this:
	# http://code.google.com/p/phusion-passenger/issues/detail?id=526
	# This appears to be a bug in older g++ versions:
	# http://gcc.gnu.org/ml/gcc-patches/2006-07/msg00861.html
	# Warnings should be suppressed with -Wno-attributes.
	def self.cc_visibility_flag_generates_warnings?
		if os_name =~ /linux/ && `#{cc} -v 2>&1` =~ /gcc version (.*?)/
			return $1 <= "4.1.2"
		else
			return false
		end
	end
	memoize :cc_visibility_flag_generates_warnings?, true

	def self.cxx_visibility_flag_generates_warnings?
		if os_name =~ /linux/ && `#{cxx} -v 2>&1` =~ /gcc version (.*?)/
			return $1 <= "4.1.2"
		else
			return false
		end
	end
	memoize :cxx_visibility_flag_generates_warnings?, true

	def self.adress_sanitizer_flag
		if cc_is_clang?
			if `#{cc} --help` =~ /-fsanitize=/
				return "-fsanitize=address"
			else
				return "-faddress-sanitizer"
			end
		else
			return nil
		end
	end
	memoize :adress_sanitizer_flag

	def self.cxx_11_flag
		# C++11 support on FreeBSD 10.0 + Clang seems to be bugged.
		# http://llvm.org/bugs/show_bug.cgi?id=18310
		return nil if os_name =~ /freebsd/

		source = %{
			struct Foo {
				Foo(Foo &&f) { }
			};
		}
		if try_compile("Checking for C++ -std=gnu++11 compiler flag", :cxx, source, '-std=gnu++11')
			return "-std=gnu++11"
		elsif try_compile("Checking for C++ -std=c++11 compiler flag", :cxx, source, '-std=c++11')
			return "-std=c++11"
		else
			return nil
		end
	end
	memoize :cxx_11_flag, true

	def self.has_rt_library?
		return try_link("Checking for -lrt support",
			:c, "int main() { return 0; }\n", '-lrt')
	end
	memoize :has_rt_library?, true

	def self.has_math_library?
		return try_link("Checking for -lmath support",
			:c, "int main() { return 0; }\n", '-lmath')
	end
	memoize :has_math_library?, true

	def self.has_alloca_h?
		return try_compile("Checking for alloca.h",
			:c, '#include <alloca.h>')
	end
	memoize :has_alloca_h?, true

	def self.has_accept4?
		return try_compile("Checking for accept4()", :c, %Q{
			#define _GNU_SOURCE
			#include <sys/socket.h>
			static void *foo = accept4;
		})
	end
	memoize :has_accept4?, true

	# C compiler flags that should be passed in order to enable debugging information.
	def self.debugging_cflags
		# According to OpenBSD's pthreads man page, pthreads do not work
		# correctly when an app is compiled with -g. It recommends using
		# -ggdb instead.
		#
		# In any case we'll always want to use -ggdb for better GDB debugging.
		if cc_is_gcc?
			return '-ggdb'
		else
			return '-g'
		end
	end

	def self.dmalloc_ldflags
		if !ENV['DMALLOC_LIBS'].to_s.empty?
			return ENV['DMALLOC_LIBS']
		end
		if os_name == "macosx"
			['/opt/local', '/usr/local', '/usr'].each do |prefix|
				filename = "#{prefix}/lib/libdmallocthcxx.a"
				if File.exist?(filename)
					return filename
				end
			end
			return nil
		else
			return "-ldmallocthcxx"
		end
	end
	memoize :dmalloc_ldflags

	def self.electric_fence_ldflags
		if os_name == "macosx"
			['/opt/local', '/usr/local', '/usr'].each do |prefix|
				filename = "#{prefix}/lib/libefence.a"
				if File.exist?(filename)
					return filename
				end
			end
			return nil
		else
			return "-lefence"
		end
	end
	memoize :electric_fence_ldflags

	def self.export_dynamic_flags
		if os_name == "linux"
			return '-rdynamic'
		else
			return nil
		end
	end


	def self.make
		return string_env('MAKE', find_command('make'))
	end
	memoize :make, true

	def self.gnu_make
		if result = string_env('GMAKE')
			return result
		else
			result = find_command('gmake')
			if !result
				result = find_command('make')
				if result
					if `#{result} --version 2>&1` =~ /GNU/
						return result
					else
						return nil
					end
				else
					return nil
				end
			else
				return result
			end
		end
	end
	memoize :gnu_make, true

	def self.xcode_select_version
		if find_command('xcode-select')
			`xcode-select --version` =~ /version (.+)\./
			return $1
		else
			return nil
		end
	end
end

end # module PhusionPassenger
