-
Jack Nagel authored
eql? should not depend on the hash value as hash values of uneql objects can collide, but eql values may only collide for objects that are actually eql. Further, python dependencies are uniquely identified by the combination of the name and imports attributes, so there is no reason to involved the expensive binary computation for simple equality checks. Fixes Homebrew/homebrew#20840.
Jack Nagel authoredeql? should not depend on the hash value as hash values of uneql objects can collide, but eql values may only collide for objects that are actually eql. Further, python dependencies are uniquely identified by the combination of the name and imports attributes, so there is no reason to involved the expensive binary computation for simple equality checks. Fixes Homebrew/homebrew#20840.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
python_dependency.rb 13.16 KiB
require 'requirement'
# We support Python 2.x and 3.x, either brewed or external.
# This requirement locates the correct CPython binary (no PyPy), provides
# support methods like `site_packages`, and writes our sitecustomize.py file.
# In `dependency_collector.rb`, special `:python` and `:python3` shortcuts are
# defined. You can specify a minimum version of the Python that needs to be
# present, but since not every package is ported to 3.x yet,
# `PythonInstalled("2")` is not satisfied by 3.x.
# In a formula that shall provide support for 2.x and 3.x, the idiom is:
# depends_on :python
# depends_on :python3 => :optional # or :recommended
#
# Todo:
# - Allow further options that choose: universal, framework?, brewed?...
class PythonInstalled < Requirement
attr_reader :min_version
attr_reader :if3then3
attr_reader :imports
attr_accessor :site_packages
attr_accessor :binary # The python.rb formula needs to set the binary
fatal true # you can still make Python optional by `depends_on :python => :optional`
class PythonVersion < Version
def major
tokens[0].to_s.to_i # Python's major.minor are always ints.
end
def minor
tokens[1].to_s.to_i
end
end
def initialize(default_version="2.6", tags=[] )
tags = [tags].flatten
# Extract the min_version if given. Default to default_version else
if /(\d+\.)*\d+/ === tags.first.to_s
@min_version = PythonVersion.new(tags.shift)
else
@min_version = PythonVersion.new(default_version)
end
# often used idiom: e.g. sipdir = "share/sip#{python.if3then3}"
if @min_version.major == 3
@if3then3 = "3"
else
@if3then3 = ""
end
# Set name according to the major version.
# The name is used to generate the options like --without-python3
@name = "python" + @if3then3
# Check if any python modules should be importable. We use a hash to store
# the corresponding name on PyPi "<import_name>" => "<name_on_PyPi>".
# Example: `depends_on :python => ['enchant' => 'pyenchant']
@imports = {}
tags.each do |tag|
if tag.kind_of? String
@imports[tag] = tag # if the module name is the same as the PyPi name
elsif tag.kind_of? Hash
@imports.merge!(tag)
end
end
# will be set later by the python_helper, because it needs the
# formula prefix to set site_packages
@site_packages = nil
super tags
end
# Note that during `satisfy` we still have the PATH as the user has set.
# We look for a brewed python or an external Python and store the loc of
# that binary for later usage. (See Formula#python)
satisfy :build_env => false do
@unsatisfied_because = ''
if binary.nil? || !binary.executable?
@unsatisfied_because += "No `#{@name}` found in your PATH! Consider to `brew install #{@name}`."
false
elsif pypy?
@unsatisfied_because += "Your #{@name} executable appears to be a PyPy, which is not supported."
false
elsif version.major != @min_version.major
@unsatisfied_because += "No Python #{@min_version.major}.x found in your PATH! --> `brew install #{@name}`?"
false
elsif version < @min_version
@unsatisfied_because += "Python version #{version} is too old (need at least #{@min_version})."
false
elsif @min_version.major == 2 && `python -c "import sys; print(sys.version_info[0])"`.strip == "3"
@unsatisfied_because += "Your `python` points to a Python 3.x. This is not supported."
else
@imports.keys.map do |module_name|
if not importable? module_name
@unsatisfied_because += "Unsatisfied dependency: #{module_name}\n"
@unsatisfied_because += "OS X System's " if from_osx?
@unsatisfied_because += "Brewed " if brewed?
@unsatisfied_because += "External " unless brewed? || from_osx?
@unsatisfied_because += "Python cannot `import #{module_name}`. Install with:\n "
unless importable? 'pip'
@unsatisfied_because += "sudo easy_install pip\n "
end
@unsatisfied_because += "pip-#{version.major}.#{version.minor} install #{@imports[module_name]}"
false
else
true
end
end.all? # all given `module_name`s have to be `importable?`
end
end
def importable? module_name
quiet_system(binary, "-c", "import #{module_name}")
end
# The full path to the python or python3 executable, depending on `version`.
def binary
@binary ||= begin
if brewed?
# If the python is brewed we always prefer it!
# Note, we don't support homebrew/versions/pythonXX.rb, though.
Formula.factory(@name).opt_prefix/"bin/python#{@min_version.major}"
else
begin
# Using the ORIGINAL_PATHS here because in superenv, the user
# installed external Python is not visible otherwise.
tmp_PATH = ENV['PATH']
ENV['PATH'] = ORIGINAL_PATHS.join(':')
which(@name)
ensure
ENV['PATH'] = tmp_PATH
end
end
end
end
# The python prefix (special cased for a brewed python to point into the opt_prefix)
def prefix
if brewed?
# Homebrew since a long while only supports frameworked python
HOMEBREW_PREFIX/"opt/#{name}/Frameworks/Python.framework/Versions/#{version.major}.#{version.minor}"
elsif from_osx?
# Python on OS X has been stripped off its includes (unless you install the CLT), therefore we use the MacOS.sdk.
Pathname.new("#{MacOS.sdk_path}/System/Library/Frameworks/Python.framework/Versions/#{version.major}.#{version.minor}")
else
# What Python knows about itself
Pathname.new(`#{binary} -c 'import sys;print(sys.prefix)'`.strip)
end
end
# Get the actual x.y.z version by asking python (or python3 if @min_version>=3)
def version
@version ||= PythonVersion.new(`#{binary} -c 'import sys;print(sys.version[:5])'`.strip)
end
# python.xy => "python2.7" is often used (and many formulae had this as `which_python`).
def xy
"python#{version.major}.#{version.minor}"
end
# Homebrew's global site-packages. The local ones (just `site_packages`) are
# populated by the python_helperg method when the `prefix` of a formula is known.
def global_site_packages
HOMEBREW_PREFIX/"lib/#{xy}/site-packages"
end
# Dir containing Python.h and others.
def incdir
if (from_osx? || brewed?) && framework?
prefix/"Headers"
else
# For all other we use Python's own standard method (works with a non-framework version, too)
Pathname.new(`#{binary} -c 'from distutils import sysconfig; print(sysconfig.get_python_inc())'`.strip)
end
end
# Dir containing e.g. libpython2.7.dylib
def libdir
if brewed? || from_osx?
if @min_version.major == 3
prefix/"lib/#{xy}/config-#{version.major}.#{version.minor}m"
else
prefix/"lib/#{xy}/config"
end
else
Pathname.new(`#{binary} -c "from distutils import sysconfig; print(sysconfig.get_config_var('LIBPL'))"`.strip)
end
end
# Pkgconfig (pc) files of python
def pkg_config_path
if from_osx?
# No matter if CLT-only or Xcode-only, the pc file is always here on OS X:
path = Pathname.new("/System/Library/Frameworks/Python.framework/Versions/#{version.major}.#{version.minor}/lib/pkgconfig")
path if path.exist?
else
prefix/"lib/pkgconfig"
end
end
# Is the Python brewed (and linked)?
def brewed?
@brewed ||= begin
require 'formula'
(Formula.factory(@name).opt_prefix/"bin/#{@name}").executable?
end
end
# Is the python the one from OS X?
def from_osx?
@from_osx ||= begin
p = `#{binary} -c "import sys; print(sys.prefix)"`.strip
p.start_with?("/System/Library/Frameworks/Python.framework")
end
end
# Is the `python` a PyPy?
def pypy?
@pypy ||= !(`#{binary} -c "import sys; print(sys.version)"`.downcase =~ /.*pypy.*/).nil?
end
def framework
# We return the path to Frameworks and not the 'Python.framework', because
# the latter is (sadly) the same for 2.x and 3.x.
if prefix.to_s =~ /^(.*\/Frameworks)\/(Python\.framework).*$/
@framework = $1
end
end
def framework?; not framework.nil? end
def universal?
@universal ||= archs_for_command(binary).universal?
end
def standard_caveats
if brewed?
"" # empty string, so we can concat this
else
<<-EOS.undent
For non-homebrew #{@name} (#{@min_version.major}.x), you need to amend your PYTHONPATH like so:
export PYTHONPATH=#{global_site_packages}:$PYTHONPATH
EOS
end
end
def modify_build_environment
# Most methods fail if we don't have a binary.
return false if binary.nil?
# Write our sitecustomize.py
file = global_site_packages/"sitecustomize.py"
ohai "Writing #{file}" if ARGV.verbose? && ARGV.debug?
[".pyc", ".pyo", ".py"].map{ |f|
global_site_packages/"sitecustomize#{f}"
}.each{ |f| f.delete if f.exist? }
file.write(sitecustomize)
# For non-system python's we add the opt_prefix/bin of python to the path.
ENV.prepend 'PATH', binary.dirname, ':' unless from_osx?
ENV['PYTHONHOME'] = nil # to avoid fuck-ups.
ENV['PYTHONPATH'] = global_site_packages.to_s unless brewed?
# Python respects the ARCHFLAGS var if set. Shall we set them here?
# ENV['ARCHFLAGS'] = ??? # FIXME
ENV.append 'CMAKE_INCLUDE_PATH', incdir, ':'
ENV.append 'PKG_CONFIG_PATH', pkg_config_path, ':' if pkg_config_path
# We don't set the -F#{framework} here, because if Python 2.x and 3.x are
# used, `Python.framework` is ambiguous. However, in the `python do` block
# we can set LDFLAGS+="-F#{framework}" because only one is temporarily set.
# Udpate distutils.cfg (later we can remove this, but people still have
# their old brewed pythons and we have to update it here)
# Todo: If Jack's formula revisions arrive, we can get rid of this here!
if brewed?
require 'formula'
file = Formula.factory(@name).prefix/"Frameworks/Python.framework/Versions/#{version.major}.#{version.minor}/lib/#{xy}/distutils/distutils.cfg"
ohai "Writing #{file}" if ARGV.verbose? && ARGV.debug?
file.delete if file.exist?
file.write <<-EOF.undent
[global]
verbose=1
[install]
force=1
prefix=#{HOMEBREW_PREFIX}
EOF
end
true
end
def sitecustomize
<<-EOF.undent
# This file is created by Homebrew and is executed on each python startup.
# Don't print from here, or else universe will collapse.
import sys
if sys.version_info[0] == #{version.major} and sys.version_info[1] == #{version.minor}:
if sys.executable.startswith('#{HOMEBREW_PREFIX}/opt/python'):
# Fix 1)
# A setuptools.pth and/or easy-install.pth sitting either in
# /Library/Python/2.7/site-packages or in
# ~/Library/Python/2.7/site-packages can inject the
# /System's Python site-packages. People then report
# "OSError: [Errno 13] Permission denied" because pip/easy_install
# attempts to install into
# /System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python
# See: https://github.com/mxcl/homebrew/issues/14712
# Fix 2)
# Remove brewed Python's hard-coded Cellar-site-packages
sys.path = [ p for p in sys.path
if not (p.startswith('/System') or
p.startswith('#{HOMEBREW_PREFIX}/Cellar/python') and p.endswith('site-packages')) ]
# Fix 3)
# Set the sys.executable to use the opt_prefix
sys.executable = '#{HOMEBREW_PREFIX}/opt/#{name}/bin/python#{version.major}.#{version.minor}'
# Fix 4)
# Make LINKFORSHARED (and python-config --ldflags) return the
# full path to the lib (yes, "Python" is actually the lib, not a
# dir) so that third-party software does not need to add the
# -F/#{HOMEBREW_PREFIX}/Frameworks switch.
# Assume Framework style build (default since months in brew)
try:
from _sysconfigdata import build_time_vars
build_time_vars['LINKFORSHARED'] = '-u _PyMac_Error #{HOMEBREW_PREFIX}/opt/#{name}/Frameworks/Python.framework/Versions/#{version.major}.#{version.minor}/Python'
except:
pass # remember: don't print here. Better to fail silently.
# Fix 5)
# For all Pythons of the right major.minor version: Tell about homebrew's
# site-packages location. This is needed for Python to parse *.pth.
import site
site.addsitedir('#{global_site_packages}')
EOF
end
def message
@unsatisfied_because
end
def <=> other
version <=> other.version
end
def to_s
binary.to_s
end
# Objects of this class are used to represent dependencies on Python and
# dependencies on Python modules, so the combination of name + imports is
# enough to identify them uniquely.
def eql?(other)
instance_of?(other.class) && name == other.name && imports == other.imports
end
def hash
[name, *imports].hash
end
end