453 lines
18 KiB
Ruby
Raw Normal View History

# typed: strict
# frozen_string_literal: true
2014-03-07 18:03:54 +00:00
module Language
# Helper functions for Python formulae.
#
# @api public
2014-03-07 18:03:54 +00:00
module Python
sig { params(python: T.any(String, Pathname)).returns(T.nilable(Version)) }
def self.major_minor_version(python)
version = `#{python} --version 2>&1`.chomp[/(\d\.\d+)/, 1]
2014-03-07 18:03:54 +00:00
return unless version
2018-09-17 02:45:00 +02:00
Version.new(version)
2014-03-07 18:03:54 +00:00
end
sig { params(python: T.any(String, Pathname)).returns(Pathname) }
2018-09-05 23:01:52 -04:00
def self.homebrew_site_packages(python = "python3.7")
HOMEBREW_PREFIX/site_packages(python)
end
sig { params(python: T.any(String, Pathname)).returns(String) }
2018-09-05 23:01:52 -04:00
def self.site_packages(python = "python3.7")
2019-03-11 20:14:03 +11:00
if (python == "pypy") || (python == "pypy3")
2018-09-05 23:01:52 -04:00
"site-packages"
else
"lib/python#{major_minor_version python}/site-packages"
end
end
sig {
params(
build: T.any(BuildOptions, Tab),
block: T.nilable(T.proc.params(python: String, version: T.nilable(Version)).void),
).void
}
def self.each_python(build, &block)
2022-05-30 04:37:09 +01:00
original_pythonpath = ENV.fetch("PYTHONPATH", nil)
2019-03-11 20:14:03 +11:00
pythons = { "python@3" => "python3",
"pypy" => "pypy",
"pypy3" => "pypy3" }
pythons.each do |python_formula, python|
python_formula = Formulary.factory(python_formula)
next if build.without? python_formula.to_s
2018-09-17 02:45:00 +02:00
version = major_minor_version python
ENV["PYTHONPATH"] = if python_formula.latest_version_installed?
2014-03-07 18:03:54 +00:00
nil
else
homebrew_site_packages(python).to_s
2014-03-07 18:03:54 +00:00
end
2017-09-24 19:24:46 +01:00
block&.call python, version
2014-03-07 18:03:54 +00:00
end
ENV["PYTHONPATH"] = original_pythonpath
end
sig { params(python: T.any(String, Pathname)).returns(T::Boolean) }
def self.reads_brewed_pth_files?(python)
return false unless homebrew_site_packages(python).directory?
2024-03-27 06:26:32 +00:00
return false unless homebrew_site_packages(python).writable?
2018-09-17 02:45:00 +02:00
2018-09-05 23:01:52 -04:00
probe_file = homebrew_site_packages(python)/"homebrew-pth-probe.pth"
begin
probe_file.atomic_write("import site; site.homebrew_was_here = True")
with_homebrew_path { quiet_system python, "-c", "import site; assert(site.homebrew_was_here)" }
ensure
probe_file.unlink if probe_file.exist?
end
end
sig { params(python: T.any(String, Pathname)).returns(Pathname) }
def self.user_site_packages(python)
Pathname.new(`#{python} -c "import site; print(site.getusersitepackages())"`.chomp)
end
sig { params(python: T.any(String, Pathname), path: T.any(String, Pathname)).returns(T::Boolean) }
def self.in_sys_path?(python, path)
2018-07-11 15:17:40 +02:00
script = <<~PYTHON
import os, sys
[os.path.realpath(p) for p in sys.path].index(os.path.realpath("#{path}"))
2018-07-11 15:17:40 +02:00
PYTHON
quiet_system python, "-c", script
end
sig { params(prefix: Pathname, python: T.any(String, Pathname)).returns(T::Array[String]) }
def self.setup_install_args(prefix, python = "python3")
2018-07-11 15:17:40 +02:00
shim = <<~PYTHON
import setuptools, tokenize
__file__ = 'setup.py'
exec(compile(getattr(tokenize, 'open', open)(__file__).read()
.replace('\\r\\n', '\\n'), __file__, 'exec'))
2018-07-11 15:17:40 +02:00
PYTHON
%W[
-c
#{shim}
--no-user-cfg
install
--prefix=#{prefix}
2018-09-05 23:01:52 -04:00
--install-scripts=#{prefix}/bin
--install-lib=#{prefix/site_packages(python)}
--single-version-externally-managed
--record=installed.txt
]
end
# Mixin module for {Formula} adding shebang rewrite features.
module Shebang
extend T::Helpers
requires_ancestor { Formula }
module_function
# A regex to match potential shebang permutations.
PYTHON_SHEBANG_REGEX = %r{^#! ?/usr/bin/(?:env )?python(?:[23](?:\.\d{1,2})?)?( |$)}
# The length of the longest shebang matching `SHEBANG_REGEX`.
PYTHON_SHEBANG_MAX_LENGTH = T.let("#! /usr/bin/env pythonx.yyy ".length, Integer)
# @private
sig { params(python_path: T.any(String, Pathname)).returns(Utils::Shebang::RewriteInfo) }
def python_shebang_rewrite_info(python_path)
Utils::Shebang::RewriteInfo.new(
PYTHON_SHEBANG_REGEX,
PYTHON_SHEBANG_MAX_LENGTH,
"#{python_path}\\1",
)
end
sig { params(formula: Formula, use_python_from_path: T::Boolean).returns(Utils::Shebang::RewriteInfo) }
def detected_python_shebang(formula = T.cast(self, Formula), use_python_from_path: false)
python_path = if use_python_from_path
"/usr/bin/env python3"
else
python_deps = formula.deps.select(&:required?).map(&:name).grep(/^python(@.+)?$/)
raise ShebangDetectionError.new("Python", "formula does not depend on Python") if python_deps.empty?
if python_deps.length > 1
raise ShebangDetectionError.new("Python", "formula has multiple Python dependencies")
end
python_dep = python_deps.first
Formula[python_dep].opt_bin/python_dep.sub("@", "")
end
python_shebang_rewrite_info(python_path)
end
end
# Mixin module for {Formula} adding virtualenv support features.
module Virtualenv
extend T::Helpers
requires_ancestor { Formula }
2024-04-30 11:10:23 +02:00
# Instantiates, creates and yields a {Virtualenv} object for use from
# {Formula#install}, which provides helper methods for instantiating and
# installing packages into a Python virtualenv.
#
# @param venv_root [Pathname, String] the path to the root of the virtualenv
# (often `libexec/"venv"`)
# @param python [String, Pathname] which interpreter to use (e.g. `"python3"`
# or `"python3.x"`)
2020-11-03 16:36:48 -05:00
# @param formula [Formula] the active {Formula}
# @return [Virtualenv] a {Virtualenv} instance
sig {
params(
venv_root: T.any(String, Pathname),
python: T.any(String, Pathname),
formula: Formula,
system_site_packages: T::Boolean,
without_pip: T::Boolean,
).returns(Virtualenv)
}
def virtualenv_create(venv_root, python = "python", formula = T.cast(self, Formula),
system_site_packages: true, without_pip: true)
# Limit deprecation to 3.12+ for now (or if we can't determine the version).
# Some used this argument for `setuptools`, which we no longer bundle since 3.12.
unless without_pip
python_version = Language::Python.major_minor_version(python)
if python_version.nil? || python_version.null? || python_version >= "3.12"
raise ArgumentError, "virtualenv_create's without_pip is deprecated starting with Python 3.12"
end
end
ENV.refurbish_args
venv = Virtualenv.new formula, venv_root, python
2024-03-07 16:20:20 +00:00
venv.create(system_site_packages:, without_pip:)
# Find any Python bindings provided by recursive dependencies
pth_contents = []
formula.recursive_dependencies do |dependent, dep|
Dependency.prune if dep.build? || dep.test?
# Apply default filter
Dependency.prune if (dep.optional? || dep.recommended?) && !dependent.build.with?(dep)
# Do not add the main site-package provided by the brewed
# Python formula, to keep the virtual-env's site-package pristine
Dependency.prune if python_names.include? dep.name
# Skip uses_from_macos dependencies as these imply no Python bindings
Dependency.prune if dep.uses_from_macos?
2018-09-17 02:45:00 +02:00
dep_site_packages = dep.to_formula.opt_prefix/Language::Python.site_packages(python)
Dependency.prune unless dep_site_packages.exist?
2018-09-17 02:45:00 +02:00
pth_contents << "import site; site.addsitedir('#{dep_site_packages}')\n"
end
(venv.site_packages/"homebrew_deps.pth").write pth_contents.join unless pth_contents.empty?
venv
end
# Returns true if a formula option for the specified python is currently
# active or if the specified python is required by the formula. Valid
# inputs are `"python"`, `"python2"` and `:python3`. Note that
# `"with-python"`, `"without-python"`, `"with-python@2"` and `"without-python@2"`
# formula options are handled correctly even if not associated with any
# corresponding depends_on statement.
sig { params(python: String).returns(T::Boolean) }
def needs_python?(python)
return true if build.with?(python)
2018-09-17 02:45:00 +02:00
(requirements.to_a | deps).any? { |r| r.name.split("/").last == python && r.required? }
end
# Helper method for the common case of installing a Python application.
# Creates a virtualenv in `libexec`, installs all `resource`s defined
2024-04-30 11:10:23 +02:00
# on the formula and then installs the formula. An options hash may be
# passed (e.g. `:using => "python"`) to override the default, guessed
2020-02-10 22:54:11 +01:00
# formula preference for python or python@x.y, or to resolve an ambiguous
# case where it's not clear whether python or python@x.y should be the
# default guess.
sig {
params(
using: T.nilable(String),
system_site_packages: T::Boolean,
without_pip: T::Boolean,
link_manpages: T::Boolean,
without: T.nilable(T.any(String, T::Array[String])),
start_with: T.nilable(T.any(String, T::Array[String])),
end_with: T.nilable(T.any(String, T::Array[String])),
).returns(Virtualenv)
}
2023-07-30 00:49:16 -07:00
def virtualenv_install_with_resources(using: nil, system_site_packages: true, without_pip: true,
link_manpages: true, without: nil, start_with: nil, end_with: nil)
python = using
if python.nil?
wanted = python_names.select { |py| needs_python?(py) }
raise FormulaUnknownPythonError, self if wanted.empty?
raise FormulaAmbiguousPythonError, self if wanted.size > 1
2018-09-17 02:45:00 +02:00
python = T.must(wanted.first)
python = "python3" if python == "python"
end
venv_resources = if without.nil? && start_with.nil? && end_with.nil?
resources
else
remaining_resources = resources.to_h { |resource| [resource.name, resource] }
slice_resources!(remaining_resources, Array(without))
start_with_resources = slice_resources!(remaining_resources, Array(start_with))
end_with_resources = slice_resources!(remaining_resources, Array(end_with))
start_with_resources + remaining_resources.values + end_with_resources
end
2024-03-07 16:20:20 +00:00
venv = virtualenv_create(libexec, python.delete("@"), system_site_packages:,
without_pip:)
venv.pip_install venv_resources
2024-03-07 16:20:20 +00:00
venv.pip_install_and_link(T.must(buildpath), link_manpages:)
venv
end
2020-10-20 12:03:48 +02:00
sig { returns(T::Array[String]) }
def python_names
%w[python python3 pypy pypy3] + Formula.names.select { |name| name.start_with? "python@" }
end
private
sig {
params(
resources_hash: T::Hash[String, Resource],
resource_names: T::Array[String],
).returns(T::Array[Resource])
}
def slice_resources!(resources_hash, resource_names)
resource_names.map do |resource_name|
resources_hash.delete(resource_name) do
raise ArgumentError, "Resource \"#{resource_name}\" is not defined in formula or is already used"
end
end
end
# Convenience wrapper for creating and installing packages into Python
# virtualenvs.
class Virtualenv
# Initializes a Virtualenv instance. This does not create the virtualenv
# on disk; {#create} does that.
#
# @param formula [Formula] the active {Formula}
# @param venv_root [Pathname, String] the path to the root of the
# virtualenv
# @param python [String, Pathname] which interpreter to use, e.g.
# "python" or "python2"
sig { params(formula: Formula, venv_root: T.any(String, Pathname), python: T.any(String, Pathname)).void }
def initialize(formula, venv_root, python)
@formula = formula
@venv_root = T.let(Pathname(venv_root), Pathname)
@python = python
end
sig { returns(Pathname) }
def root
@venv_root
end
sig { returns(Pathname) }
def site_packages
@venv_root/Language::Python.site_packages(@python)
end
# Obtains a copy of the virtualenv library and creates a new virtualenv on disk.
#
# @return [void]
sig { params(system_site_packages: T::Boolean, without_pip: T::Boolean).void }
2023-07-30 00:49:16 -07:00
def create(system_site_packages: true, without_pip: true)
return if (@venv_root/"bin/python").exist?
args = ["-m", "venv"]
args << "--system-site-packages" if system_site_packages
2023-07-30 00:49:16 -07:00
args << "--without-pip" if without_pip
@formula.system @python, *args, @venv_root
# Robustify symlinks to survive python patch upgrades
@venv_root.find do |f|
next unless f.symlink?
next unless f.readlink.expand_path.to_s.start_with? HOMEBREW_CELLAR
2018-09-17 02:45:00 +02:00
rp = f.realpath.to_s
2020-11-16 22:18:56 +01:00
version = rp.match %r{^#{HOMEBREW_CELLAR}/python@(.*?)/}o
version = "@#{version.captures.first}" unless version.nil?
new_target = rp.sub %r{#{HOMEBREW_CELLAR}/python#{version}/[^/]+}, Formula["python#{version}"].opt_prefix
2016-09-10 10:38:35 +01:00
f.unlink
f.make_symlink new_target
end
Pathname.glob(@venv_root/"lib/python*/orig-prefix.txt").each do |prefix_file|
prefix_path = prefix_file.read
2020-11-16 22:18:56 +01:00
version = prefix_path.match %r{^#{HOMEBREW_CELLAR}/python@(.*?)/}o
version = "@#{version.captures.first}" unless version.nil?
prefix_path.sub! %r{^#{HOMEBREW_CELLAR}/python#{version}/[^/]+}, Formula["python#{version}"].opt_prefix
prefix_file.atomic_write prefix_path
end
2023-07-30 00:49:16 -07:00
# Reduce some differences between macOS and Linux venv
lib64 = @venv_root/"lib64"
lib64.make_symlink "lib" unless lib64.exist?
if (cfg_file = @venv_root/"pyvenv.cfg").exist?
cfg = cfg_file.read
framework = "Frameworks/Python.framework/Versions"
cfg.match(%r{= *(#{HOMEBREW_CELLAR}/(python@[\d.]+)/[^/]+(?:/#{framework}/[\d.]+)?/bin)}) do |match|
cfg.sub! match[1].to_s, Formula[match[2]].opt_bin
cfg_file.atomic_write cfg
end
end
2023-07-30 00:49:16 -07:00
# Remove unnecessary activate scripts
(@venv_root/"bin").glob("[Aa]ctivate*").map(&:unlink)
end
# Installs packages represented by `targets` into the virtualenv.
#
# @param targets [String, Pathname, Resource,
# Array<String, Pathname, Resource>] (A) token(s) passed to `pip`
# representing the object to be installed. This can be a directory
# containing a setup.py, a {Resource} which will be staged and
# installed, or a package identifier to be fetched from PyPI.
# Multiline strings are allowed and treated as though they represent
# the contents of a `requirements.txt`.
# @return [void]
sig {
params(
targets: T.any(String, Pathname, Resource, T::Array[T.any(String, Pathname, Resource)]),
build_isolation: T::Boolean,
).void
}
def pip_install(targets, build_isolation: true)
2020-07-13 22:48:53 +10:00
targets = Array(targets)
targets.each do |t|
if t.is_a?(Resource)
t.stage do
target = Pathname.pwd
target /= t.downloader.basename if t.url&.end_with?("-none-any.whl")
do_install(target, build_isolation:)
end
else
t = t.lines.map(&:strip) if t.is_a?(String) && t.include?("\n")
2024-03-07 16:20:20 +00:00
do_install(t, build_isolation:)
end
end
end
# Installs packages represented by `targets` into the virtualenv, but
# unlike {#pip_install} also links new scripts to {Formula#bin}.
#
# @param (see #pip_install)
# @return (see #pip_install)
sig {
params(
targets: T.any(String, Pathname, Resource, T::Array[T.any(String, Pathname, Resource)]),
link_manpages: T::Boolean,
build_isolation: T::Boolean,
).void
}
def pip_install_and_link(targets, link_manpages: true, build_isolation: true)
bin_before = Dir[@venv_root/"bin/*"].to_set
man_before = Dir[@venv_root/"share/man/man*/*"].to_set if link_manpages
2024-03-07 16:20:20 +00:00
pip_install(targets, build_isolation:)
bin_after = Dir[@venv_root/"bin/*"].to_set
bin_to_link = (bin_after - bin_before).to_a
@formula.bin.install_symlink(bin_to_link)
return unless link_manpages
man_after = Dir[@venv_root/"share/man/man*/*"].to_set
man_to_link = (man_after - man_before).to_a
man_to_link.each do |manpage|
(@formula.man/Pathname.new(manpage).dirname.basename).install_symlink manpage
end
end
private
sig {
params(
targets: T.any(String, Pathname, T::Array[T.any(String, Pathname)]),
build_isolation: T::Boolean,
).void
}
def do_install(targets, build_isolation: true)
2020-07-13 22:48:53 +10:00
targets = Array(targets)
2024-03-07 16:20:20 +00:00
args = @formula.std_pip_args(prefix: false, build_isolation:)
2023-07-30 00:49:16 -07:00
@formula.system @python, "-m", "pip", "--python=#{@venv_root}/bin/python", "install", *args, *targets
end
2017-10-21 19:52:43 +02:00
end
end
end
end