[Distutils] The Ubuntu (Debian?) package maintainers for distutils and setuptools broke PYTHONUSERBASE

Larry Hastings larry at hastings.org
Mon Mar 30 19:18:45 CEST 2009


I'm trying to add a lightweight "virtualenv" to a local build system 
using PYTHONUSERBASE.  Unfortunately the package maintainers for 
distutils and setuptools broke it on my Ubuntu laptop.  I have 
incontrovertable proof!

PYTHONUSERBASE doesn't scan the exact directory you set it to; you're 
pointing it at a "prefix" directory, and it looks in directories under 
there.  Well, *directory*, really: PYTHONUSERBASE only causes CPython to 
scan one additional directory for packages.  That directory is: 
"{prefix}/lib/python2.6/site-packages".  This is true whether you use 
the Debian packaged build or if you build Python yourself from source.

AFAICT distutils uses distutils.sysconfig.get_python_lib() to decide 
where you should install a site package.  Depending on the inputs, it 
will give you one of two directories.  If it's a "standard library", 
it'll give you "{prefix}/lib/python2.6".

However!  If you call get_python_lib() saying it's *not* a standard 
library, you get different results.  If you build CPython from scratch 
you'll get  "{prefix}/lib/python2.6/site-packages".  If you use the 
Ubuntu Python 2.6 package, you'll get 
"{prefix}/lib/python2.6/dist-packages".  But PYTHONUSERBASE still only 
looks in the "site-packages" directory.  It ignores this directory and 
therefore doesn't pick up your packages.

A more obscure but more widespread bug: consider that PYTHONUSERBASE 
also ignores the "standard library" directory returned by 
get_python_lib().  If you wanted to have your own local version of a 
"standard library", you couldn't put it in "{prefix}/lib/python2.6".  
This is true whether you build CPython from scratch or use the Ubuntu 
packages.

setuptools under Ubuntu breaks PYTHONUSERBASE in a similar but different 
way.  If you install the setuptools egg by hand, Setuptools will install 
packages into "{prefix}/lib/python2.6/site-packages".  If you use the 
Ubuntu package for setuptools, setuptools will install packages into 
"{prefix}/local/lib/python2.6/dist-packages".  Again, this is a 
directory PYTHONUSERBASE ignores.

In my opinion the best way to fix this would be to add a new environment 
variable and deprecate the old one.  I nominate "PYTHONUSERBASES"--note 
the plural.  PYTHONUSERBASES would support a 
local-path-separator-separated list of directories, all of which would 
be used as site package directories.  For each directory on the path, we 
would add *the same list of subdirectories* that site.addsitepackages() 
does.

Failing that, PYTHONUSERBASE should be at least be changed so it adds 
the same directories as site.addsitepackages().

I would be happy to contribute a patch to do either of these, for Python 
2.x and 3.x.


What follows is my test case where I figured all this out.  I started by 
creating a PYTHONUSERBASE directory, which in my late-night hacking 
fever I called "/home/larry/pwned".  Inside I created subdirectories 
matching every directory I'd ever seen scanned for site packages, like 
"lib/python2.6/site-packages".  Then for each directory I added a 
do-nothing module named for the directory it was in, with a "UB" on the 
front so I knew it came from my PYTHONUSERBASE.  For example, one file 
was called 
"/home/larry/pwned/local/lib/python2.6/dist-packages/UBlocallibpythondistpackages.py".

Next, I added those same files to the equivalent directories in "/usr", 
only with "SYSTEM" on the front.  For instance, 
"/usr/lib/python2.6/site-packages/SYSTEMlibpythonsitepackages.py"

Next, I built Python 2.6 from source, with a prefix directory of 
"/home/larry/src/python/userbase/release", and installed setuptools from 
the egg on cheeseshop.  I then added these same files one last time, and 
again with SYSTEM on the front, to my locally-built Python's prefix 
directory.

Finally I wrote a Python script that tried to import every one of these 
modules, and ran it as follows:
% PYTHONUSERBASE=/home/larry/pwned python findcookies.py
% PYTHONUSERBASE=/home/larry/pwned ./python findcookies.py

Here's the output.  First, the results from running my built-from-source 
Python.  Hopefully you're reading this in a fixed-point font on a wide 
screen:
--
% PYTHONUSERBASE=/home/larry/pwned 
/home/larry/src/python/userbase/release/bin/python 
/home/larry/findcookies.py

------------------------------------------------------------------------------
Where does distutils say we should install?
  Calling distutils.sysconfig.get_python_lib()
  with two different prefixes (sys.prefix and "/home/larry/pwned")
  and all combinations of its two boolean arguments:

  du.sc.gpl(True , True , '/home/larry/src/python/userbase/release')  = 
'/home/larry/src/python/userbase/release/lib/python2.6'
  du.sc.gpl(True , True , '/home/larry/pwned')                        = 
'/home/larry/pwned/lib/python2.6'

  du.sc.gpl(True , False, '/home/larry/src/python/userbase/release')  = 
'/home/larry/src/python/userbase/release/lib/python2.6/site-packages'
  du.sc.gpl(True , False, '/home/larry/pwned')                        = 
'/home/larry/pwned/lib/python2.6/site-packages'

  du.sc.gpl(False, True , '/home/larry/src/python/userbase/release')  = 
'/home/larry/src/python/userbase/release/lib/python2.6'
  du.sc.gpl(False, True , '/home/larry/pwned')                        = 
'/home/larry/pwned/lib/python2.6'

  du.sc.gpl(False, False, '/home/larry/src/python/userbase/release')  = 
'/home/larry/src/python/userbase/release/lib/python2.6/site-packages'
  du.sc.gpl(False, False, '/home/larry/pwned')                        = 
'/home/larry/pwned/lib/python2.6/site-packages'


------------------------------------------------------------------------------
Where does setup_tools say we should install?
  Expanding 
setuptools.command.easy_install.easy_install.INSTALL_SCHEMES[os.name]["install_dir"]
  with two different prefixes (sys.prefix and "/home/larry/pwned"):

  /home/larry/src/python/userbase/release/lib/python2.6/site-packages
  /home/larry/pwned/lib/python2.6/site-packages

------------------------------------------------------------------------------
Finally: trying to load a module from every possible site-packages 
directory.
  There are five columns in the output; here's what they mean.

  +----------- Marked with "*" if distutils.sysconfig.get_python_lib
  |            told us to use this directory.
  |
  | +--------- Marked with "EZ" if 
setuptools.command.easy_install.easy_install.INSTALL_SCHEMES
  | |          told us to use this directory.
  | |
  | |  +------ Could we load this module?
  | |  |
  | |  |      +--- What silly name did I give     +--- What directory did I
  | |  |      |    to this module?                |    stick this module 
into?
  | |  |      |                                   |
  v v  v      v                                   v
  *    True   SYSTEMlibpython                     
/home/larry/src/python/userbase/release/lib/python2.6
       True   SYSTEMlibsitepython                 
/home/larry/src/python/userbase/release/lib/site-python
       False  SYSTEMlibdistpython                 
/home/larry/src/python/userbase/release/lib/dist-python
  * EZ True   SYSTEMlibpythonsitepackages         
/home/larry/src/python/userbase/release/lib/python2.6/site-packages
       False  SYSTEMlibpythondistpackages         
/home/larry/src/python/userbase/release/lib/python2.6/dist-packages
       False  SYSTEMlocallibpythondistpackages    
/home/larry/src/python/userbase/release/local/lib/python2.6/dist-packages

  *    False  UBlibpython                         
/home/larry/pwned/lib/python2.6
       False  UBlibsitepython                     
/home/larry/pwned/lib/site-python
       False  UBlibdistpython                     
/home/larry/pwned/lib/dist-python
  * EZ True   UBlibpythonsitepackages             
/home/larry/pwned/lib/python2.6/site-packages
       False  UBlibpythondistpackages             
/home/larry/pwned/lib/python2.6/dist-packages
       False  UBlocallibpythondistpackages        
/home/larry/pwned/local/lib/python2.6/dist-packages
--

Next, the output from the CPython and setuptools from the Ubuntu packages:
--
% PYTHONUSERBASE=/home/larry/pwned /usr/bin/python 
/home/larry/findcookies.py

------------------------------------------------------------------------------
Where does distutils say we should install?
  Calling distutils.sysconfig.get_python_lib()
  with two different prefixes (sys.prefix and "/home/larry/pwned")
  and all combinations of its two boolean arguments:

  du.sc.gpl(True , True , '/usr')                                     = 
'/usr/lib/python2.6'
  du.sc.gpl(True , True , '/home/larry/pwned')                        = 
'/home/larry/pwned/lib/python2.6'

  du.sc.gpl(True , False, '/usr')                                     = 
'/usr/lib/python2.6/dist-packages'
  du.sc.gpl(True , False, '/home/larry/pwned')                        = 
'/home/larry/pwned/lib/python2.6/dist-packages'

  du.sc.gpl(False, True , '/usr')                                     = 
'/usr/lib/python2.6'
  du.sc.gpl(False, True , '/home/larry/pwned')                        = 
'/home/larry/pwned/lib/python2.6'

  du.sc.gpl(False, False, '/usr')                                     = 
'/usr/lib/python2.6/dist-packages'
  du.sc.gpl(False, False, '/home/larry/pwned')                        = 
'/home/larry/pwned/lib/python2.6/dist-packages'


------------------------------------------------------------------------------
Where does setup_tools say we should install?
  Expanding 
setuptools.command.easy_install.easy_install.INSTALL_SCHEMES[os.name]["install_dir"]
  with two different prefixes (sys.prefix and "/home/larry/pwned"):

  /usr/local/lib/python2.6/dist-packages
  /home/larry/pwned/local/lib/python2.6/dist-packages

------------------------------------------------------------------------------
Finally: trying to load a module from every possible site-packages 
directory.
  There are five columns in the output; here's what they mean.

  +----------- Marked with "*" if distutils.sysconfig.get_python_lib
  |            told us to use this directory.
  |
  | +--------- Marked with "EZ" if 
setuptools.command.easy_install.easy_install.INSTALL_SCHEMES
  | |          told us to use this directory.
  | |
  | |  +------ Could we load this module?
  | |  |
  | |  |      +--- What silly name did I give     +--- What directory did I
  | |  |      |    to this module?                |    stick this module 
into?
  | |  |      |                                   |
  v v  v      v                                   v
  *    True   SYSTEMlibpython                     /usr/lib/python2.6
       False  SYSTEMlibsitepython                 /usr/lib/site-python
       True   SYSTEMlibdistpython                 /usr/lib/dist-python
       False  SYSTEMlibpythonsitepackages         
/usr/lib/python2.6/site-packages
  *    True   SYSTEMlibpythondistpackages         
/usr/lib/python2.6/dist-packages
    EZ True   SYSTEMlocallibpythondistpackages    
/usr/local/lib/python2.6/dist-packages

  *    False  UBlibpython                         
/home/larry/pwned/lib/python2.6
       False  UBlibsitepython                     
/home/larry/pwned/lib/site-python
       False  UBlibdistpython                     
/home/larry/pwned/lib/dist-python
       True   UBlibpythonsitepackages             
/home/larry/pwned/lib/python2.6/site-packages
  *    False  UBlibpythondistpackages             
/home/larry/pwned/lib/python2.6/dist-packages
    EZ False  UBlocallibpythondistpackages        
/home/larry/pwned/local/lib/python2.6/dist-packages
--

For what it's worth, here's my horrible hacked-together script:
--
import distutils.sysconfig
import os
import sys


paths = {}

print """
%% PYTHONUSERBASE=%s %s %s

------------------------------------------------------------------------------
Where does distutils say we should install?
  Calling distutils.sysconfig.get_python_lib()
  with two different prefixes (sys.prefix and "/home/larry/pwned")
  and all combinations of its two boolean arguments:
""".strip() % (os.environ["PYTHONUSERBASE"], sys.executable, sys.argv[0])
print


for platformSpecific in (True, False):
    for standardLib in (True, False):
        for prefix in (sys.prefix, "/home/larry/pwned"):
            path = distutils.sysconfig.get_python_lib(platformSpecific, 
standardLib, prefix)
            print ("  du.sc.gpl(" + str(platformSpecific).ljust(5) + ", 
" + str(standardLib).ljust(5) + ", " + repr(prefix) + ")").ljust(69), 
"=", repr(path)
            paths[path] = "*   "
        print

print

print """
------------------------------------------------------------------------------
Where does setup_tools say we should install?
  Expanding 
setuptools.command.easy_install.easy_install.INSTALL_SCHEMES[os.name]["install_dir"]
  with two different prefixes (sys.prefix and "/home/larry/pwned"):
""".strip()
print

import setuptools.command.easy_install as ei
for where, base in (("system", sys.prefix), ("userbase", 
"/home/larry/pwned")):
    path = 
ei.easy_install.INSTALL_SCHEMES[os.name]["install_dir"].replace("$base", 
base).replace("$py_version_short", "2.6")
    print " ", path
    if path in paths:
        paths[path] = "* EZ"
    else:
        paths[path] = "  EZ"

print

print """
------------------------------------------------------------------------------
Finally: trying to load a module from every possible site-packages 
directory.
  There are five columns in the output; here's what they mean.

  +----------- Marked with "*" if distutils.sysconfig.get_python_lib
  |            told us to use this directory.
  |
  | +--------- Marked with "EZ" if 
setuptools.command.easy_install.easy_install.INSTALL_SCHEMES
  | |          told us to use this directory.
  | |
  | |  +------ Could we load this module?
  | |  |
  | |  |      +--- What silly name did I give     +--- What directory did I
  | |  |      |    to this module?                |    stick this module 
into?
  | |  |      |                                   |
  v v  v      v                                   v
""".strip()

for prefix, pathprefix, where in (("SYSTEM", 
os.path.normpath(sys.prefix) + "/", "system  "), ("UB", 
"/home/larry/pwned/", "userbase")):
    for _name, path in (
        ("libpython", "lib/python2.6"),
        ("libsitepython", "lib/site-python"),
        ("libdistpython", "lib/dist-python"),
        ("libpythonsitepackages", "lib/python2.6/site-packages"),
        ("libpythondistpackages", "lib/python2.6/dist-packages"),
    # nobody ever told me to use one of these four, so I'm removing 'em.
    #    ("locallibpython", "local/lib/python2.6"),
    #    ("locallibsitepython", "local/lib/site-python"),
    #    ("locallibdistpython", "local/lib/dist-python"),
    #    ("locallibpythonsitepackages", 
"local/lib/python2.6/site-packages"),
        ("locallibpythondistpackages", "local/lib/python2.6/dist-packages"),
        ):
        name = prefix + _name
        fullpath = os.path.join(pathprefix, path)
        filename = os.path.join(fullpath, name + ".py")
        assert os.path.isfile(filename), "Your test is wrong, %s does 
not exist!" % repr(filename)
        s = "import {0}".format(name)
        try:
            exec s
            worked = True
        except ImportError:
            worked = False
        starred = paths.get(fullpath, "    ")
        print " ", starred, str(worked).ljust(6), name.ljust(35), fullpath

    if 0:
        #print path
        if 1:
            name = name.replace("UB", "SYSTEM")
            if not os.path.isdir(path):
                os.makedirs(path)
            filename = path + "/" + name + ".py"
            filename = filename.replace("/python/", "/python2.6/")
            #print filename
            #continue
            f = open(filename, "wt")
            f.write('cookie = "PIXIE UNICORN"\n')
            f.close()
    print
--

If you read this far, I congratulate you on your powers of concentration.

Hope this helps,


/larry/


More information about the Distutils-SIG mailing list