From 2783adec4a906c8fb5c45aa1305b1460b5bc8a5b Mon Sep 17 00:00:00 2001 From: Tim D. Smith Date: Fri, 22 Jul 2016 23:02:52 -0700 Subject: Add helper class for Python virtualenvs --- Library/Homebrew/language/python.rb | 135 +++++++++++++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 2 deletions(-) (limited to 'Library/Homebrew/language/python.rb') diff --git a/Library/Homebrew/language/python.rb b/Library/Homebrew/language/python.rb index 3c84e5362..3abe77ead 100644 --- a/Library/Homebrew/language/python.rb +++ b/Library/Homebrew/language/python.rb @@ -1,4 +1,5 @@ require "utils" +require "language/python_virtualenv_constants" module Language module Python @@ -96,5 +97,135 @@ module Language def self.package_available?(python, module_name) quiet_system python, "-c", "import #{module_name}" end - end -end + + # Mixin module for {Formula} adding virtualenv support features. + module Virtualenv + def self.included(base) + base.class_eval do + resource "homebrew-virtualenv" do + url PYTHON_VIRTUALENV_URL + sha256 PYTHON_VIRTUALENV_SHA256 + end + end + end + + # 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] which interpreter to use (e.g. "python" + # or "python3") + # @param formula [Formula] the active Formula + # @return [Virtualenv] a {Virtualenv} instance + def virtualenv_create(venv_root, python = "python", formula = self) + venv = Virtualenv.new formula, venv_root, python + venv.create + venv + end + + # Helper method for the common case of installing a Python application. + # Creates a virtualenv in `libexec`, installs all `resource`s defined + # on the formula, and then installs the formula. + def virtualenv_install_with_resources + venv = virtualenv_create(libexec) + venv.pip_install resources + venv.link_scripts(bin) { venv.pip_install buildpath } + venv + 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] which interpreter to use; i.e. "python" or + # "python3" + def initialize(formula, venv_root, python) + @formula = formula + @venv_root = Pathname.new(venv_root) + @python = python + end + + # Obtains a copy of the virtualenv library and creates a new virtualenv + # on disk. + # @return [void] + def create + return if (@venv_root/"bin/python").exist? + + @formula.resource("homebrew-virtualenv").stage do |stage| + old_pythonpath = ENV.delete "PYTHONPATH" + begin + xy = Language::Python.major_minor_version(@python) + staging = Pathname.new(stage.staging.tmpdir) + ENV.prepend_create_path "PYTHONPATH", staging/"target/lib/python#{xy}/site-packages" + @formula.system @python, *Language::Python.setup_install_args(staging/"target") + @formula.system @python, "-s", staging/"target/bin/virtualenv", "-p", @python, @venv_root + ensure + ENV["PYTHONPATH"] = old_pythonpath + end + end + + # Robustify symlinks to survive python3 patch upgrades + @venv_root.find do |f| + next unless f.symlink? + if (rp = f.realpath.to_s).start_with? HOMEBREW_CELLAR + python = rp.include?("python3") ? "python3" : "python" + new_target = rp.sub %r{#{HOMEBREW_CELLAR}/#{python}/[^/]+}, Formula[python].opt_prefix + f.unlink + f.make_symlink new_target + end + end + end + + # Installs packages represented by `targets` into the virtualenv. + # @param targets [String, Pathname, Resource, + # Array] (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] + def pip_install(targets) + targets = [targets] unless targets.is_a? Array + targets.each do |t| + if t.respond_to? :stage + next if t.name == "homebrew-virtualenv" + t.stage { do_install Pathname.pwd } + else + t = t.lines.map(&:strip) if t.respond_to?(:lines) && t =~ /\n/ + do_install t + end + end + end + + # Compares the venv bin directory before and after executing a block, + # and symlinks any new scripts into `destination`. + # Use like: venv.link_scripts(bin) { venv.pip_install my_package } + # @param destination [Pathname, String] Destination into which new + # scripts should be linked. + # @return [void] + def link_scripts(destination) + bin_before = Dir[@venv_root/"bin/*"].to_set + yield + bin_after = Dir[@venv_root/"bin/*"].to_set + destination = Pathname.new(destination) + destination.install_symlink((bin_after - bin_before).to_a) + end + + private + + def do_install(targets) + targets = [targets] unless targets.is_a? Array + @formula.system @venv_root/"bin/pip", "install", + "-v", "--no-deps", "--no-binary", ":all:", + *targets + end + end # class Virtualenv + end # module Virtualenv + end # module Python +end # module Language -- cgit v1.2.3