# -*- ruby encoding: utf-8 -*- class RubyPython::InvalidInterpreter < Exception end ## # An instance of this class represents information about a particular # \Python interpreter. # # This class represents the current Python interpreter. # A class that represents a \Python executable. # # End users may get the instance that represents the current running \Python # interpreter (from +RubyPython.python+), but should not directly # instantiate this class. class RubyPython::Interpreter ## # Compare the current Interpreter to the provided Interpreter or # configuration hash. A configuration hash will be converted to an # Interpreter object before being compared. # :python_exe basename is used. If comparing against another Interpreter # object, the Interpreter basename and version are used. def ==(other) other = self.class.new(other) if other.kind_of? Hash return false unless other.kind_of? self.class (self.version == other.version) && (self.version_name == other.version_name) end ## # Create a new instance of an Interpreter instance describing a particular # \Python executable and shared library. # # Expects a hash that matches the configuration options provided to # RubyPython.start; currently only one value is recognized in that hash: # # * :python_exe: Specifies the name of the python executable to # run. def initialize(options = {}) @python_exe = options[:python_exe] # Windows: 'C:\\Python27\python.exe' # Mac OS X: '/usr/bin/ # The default interpreter might be python3 on some systems rc, majorversion = runpy "import sys; print(sys.version_info[0])" if majorversion == "3" warn "The python interpreter is python 3, switching to python2" @python_exe = "python2" end rc, @python = runpy "import sys; print sys.executable" if rc.exitstatus.nonzero? raise RubyPython::InvalidInterpreter, "An invalid interpreter was specified." end rc, @version = runpy "import sys; print '%d.%d' % sys.version_info[:2]" rc, @sys_prefix = runpy "import sys; print sys.prefix" if ::FFI::Platform.windows? flat_version = @version.tr('.', '') basename = File.basename(@python, '.exe') if basename =~ /(?:#{@version}|#{flat_version})$/ @version_name = basename else @version_name = "#{basename}#{flat_version}" end else basename = File.basename(@python) if basename =~ /#{@version}/ @version_name = basename elsif basename.end_with?("2") @version_name = "#{basename[0..-2]}#{@version}" else @version_name = "#{basename}#{@version}" end end @library = find_python_lib end def find_python_lib # By default, the library name will be something like # libpython2.6.so, but that won't always work. @libbase = "#{::FFI::Platform::LIBPREFIX}#{@version_name}" @libext = ::FFI::Platform::LIBSUFFIX @libname = "#{@libbase}.#{@libext}" # We may need to look in multiple locations for Python, so let's # build this as an array. @locations = [ File.join(@sys_prefix, "lib", @libname) ] if ::FFI::Platform.mac? # On the Mac, let's add a special case that has even a different # @libname. This may not be fully useful on future versions of OS # X, but it should work on 10.5 and 10.6. Even if it doesn't, the # next step will (/usr/lib/libpython.dylib is a symlink # to the correct location). @locations << File.join(@sys_prefix, "Python") # Let's also look in the location that was originally set in this # library: File.join(@sys_prefix, "lib", "#{@realname}", "config", @libname) end if ::FFI::Platform.unix? # On Unixes, let's look in some standard alternative places, too. # Just in case. Some Unixes don't include a .so symlink when they # should, so let's look for the base cases of .so.1 and .so.1.0, too. [ @libname, "#{@libname}.1", "#{@libname}.1.0" ].each do |name| if ::FFI::Platform::ARCH != 'i386' @locations << File.join("/opt/local/lib64", name) @locations << File.join("/opt/lib64", name) @locations << File.join("/usr/local/lib64", name) @locations << File.join("/usr/lib64", name) @locations << File.join("/usr/lib/x86_64-linux-gnu", name) end @locations << File.join("/opt/local/lib", name) @locations << File.join("/opt/lib", name) @locations << File.join("/usr/local/lib", name) @locations << File.join("/usr/lib", name) end end if ::FFI::Platform.windows? # On Windows, the appropriate DLL is usually be found in # %SYSTEMROOT%\system or %SYSTEMROOT%\system32; as a fallback we'll # use C:\Windows\system{,32} as well as the install directory and the # install directory + libs. system_root = File.expand_path(ENV['SYSTEMROOT']).gsub(/\\/, '/') @locations << File.join(system_root, 'system', @libname) @locations << File.join(system_root, 'system32', @libname) @locations << File.join("C:/WINDOWS", "System", @libname) @locations << File.join("C:/WINDOWS", "System32", @libname) @locations << File.join(sys_prefix, @libname) @locations << File.join(sys_prefix, 'libs', @libname) @locations << File.join(system_root, "SysWOW64", @libname) @locations << File.join("C:/WINDOWS", "SysWOW64", @libname) end # Let's add alternative extensions; again, just in case. @locations.dup.each do |location| path = File.dirname(location) base = File.basename(location, ".#{@libext}") @locations << File.join(path, "#{base}.so") # Standard Unix @locations << File.join(path, "#{base}.dylib") # Mac OS X @locations << File.join(path, "#{base}.dll") # Windows end # Remove redundant locations @locations.uniq! library = nil @locations.each do |location| if File.exists? location library = location break end end library end private :find_python_lib def valid? if @python.nil? or @python.empty? false elsif @library.nil? or @library.empty? false else true end end ## # The name of the \Python executable that is used. This is the value of # 'sys.executable' for the \Python interpreter provided in # :python_exe or 'python' if it is not provided. # # On Mac OS X Lion (10.7), this value is '/usr/bin/python' for 'python'. attr_reader :python ## # The version of the \Python interpreter. This is a decimalized version of # 'sys.version_info[:2]' (such that \Python 2.7.1 is reported as '2.7'). attr_reader :version ## # The system prefix for the \Python interpreter. This is the value of # 'sys.prefix'. attr_reader :sys_prefix ## # The basename of the \Python interpreter with a version number. This is # mostly an intermediate value used to find the shared \Python library, # but /usr/bin/python is often a link to /usr/bin/python2.7 so it may be # of value. Note that this does *not* include the full path to the # interpreter. attr_reader :version_name # The \Python library. attr_reader :library # Run a Python command-line command. def runpy(command) i = @python || @python_exe || 'python' if ::FFI::Platform.windows? o = %x(#{i} -c "#{command}" 2> NUL:) else o = %x(#{i} -c "#{command}" 2> /dev/null) end [ $?, o.chomp ] end private :runpy def inspect(debug = false) if debug debug_s elsif @python "#<#{self.class}: #{python} v#{version} #{sys_prefix} #{version_name}>" else "#<#{self.class}: invalid interpreter>" end end def debug_s(format = nil) system = "" system << "windows " if ::FFI::Platform.windows? system << "mac " if ::FFI::Platform.mac? system << "unix " if ::FFI::Platform.unix? system << "unknown " if system.empty? case format when :report s = <<-EOS python_exe: #{@python_exe} python: #{@python} version: #{@version} sys_prefix: #{@sys_prefix} version_name: #{@version_name} platform: #{system.chomp} library: #{@library.inspect} libbase: #{@libbase} libext: #{@libext} libname: #{@libname} locations: #{@locations.inspect} EOS else s = "#<#{self.class}: " s << "python_exe=#{@python_exe.inspect} " s << "python=#{@python.inspect} " s << "version=#{@version.inspect} " s << "sys_prefix=#{@sys_prefix.inspect} " s << "version_name=#{@version_name.inspect} " s << system s << "library=#{@library.inspect} " s << "libbase=#{@libbase.inspect} " s << "libext=#{@libext.inspect} " s << "libname=#{@libname.inspect} " s << "locations=#{@locations.inspect}" end s end end