[Distutils] Install a script to <prefix>/sbin instead of <prefix>/bin

Michael Jansen info at michael-jansen.biz
Mon Dec 2 15:14:52 CET 2013


Hi

I am currently working on the cobbler (http://cobblerd.org) setup.py and trying to improve it. It 
currently is only capable of installing to /usr for several reasons. Since i would like to have 
virtualenv support when running it i am trying to change that. While doing that i managed to meet 
one of the standard problems with distutils/setuptools - a search returns results of 2004 or older - 
which still seems to be unsolved.

I would be willing to change that but my recent research leaves me a bit confused with all that 
setuptools is not dead, distlibs, tuf and wheels stuff.

So here is the question. If i am willing to do the work is it possible to add something like 
sbin_scripts (or admin_scripts?) to distutils?

I locally solved the problem like that http://pastebin.kde.org/pqrwrud1p (or attached).

The implementation expands INSTALL_SCHEMA and reuses install_scripts to install those 
sbin_scripts too. That could be debatable. Perhaps a dedicated build|install_xxx_scripts command.

I want to add that fhs also has a libexec folder (usually /usr/lib/<project>/libexec these days) 
destined for scripts that are supposed to be only called by other scripts/programs not manually.

And cobbler for installs python scripts for modwsgi into /srv/www/cobbler/ . So i guess just adding 
sbin_scripts is not really the solution. Perhaps something more flexible is needed.

Or should i wait for nextgen distlib based swiss knife python build tool and for now keep my local 
additions (there are more).

-- 
Michael Jansen
http://michael-jansen.biz
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/distutils-sig/attachments/20131202/664d1719/attachment-0001.html>
-------------- next part --------------
from distutils.command import install as dist_install
from distutils.command.build   import build as _build
from distutils.command.build_scripts   import build_scripts as _build_scripts
from distutils.command.build_scripts   import convert_path, newer, first_line_re

from setuptools.command.install import install as _install
from setuptools.command.install_scripts import install_scripts as _install_scripts
from setuptools import setup as _setup
from setuptools.dist import Distribution as _Distribution

from stat import ST_MODE
import sys
import os
from distutils import log

if sys.version < "2.2":
    dist_install.WINDOWS_SCHEME['sbin_scripts'] = '$base/Scripts'
else:
    dist_install.WINDOWS_SCHEME['sbin_scripts'] = '$base/Scripts'

dist_install.INSTALL_SCHEMES['unix_prefix']['sbin_scripts'] = '$base/sbin'
dist_install.INSTALL_SCHEMES['unix_home']['sbin_scripts'] = '$base/sbin'
dist_install.INSTALL_SCHEMES['unix_user']['sbin_scripts'] = '$userbase/sbin'
dist_install.INSTALL_SCHEMES['nt_user']['sbin_scripts'] = '$userbase/Scripts'
dist_install.INSTALL_SCHEMES['os2']['sbin_scripts'] = '$base/Scripts'
dist_install.INSTALL_SCHEMES['os2_home']['sbin_scripts'] = '$userbase/sbin'

# The keys to an installation scheme; if any new types of files are to be
# installed, be sure to add an entry to every installation scheme above,
# and to SCHEME_KEYS here.
dist_install.SCHEME_KEYS = ('purelib', 'platlib', 'headers', 'scripts', 'data') + ('sbin_scripts',)

class install(_install):
    """Enhance install target aimed for inclusion upstream."""

    user_options = _install.user_options + [
        ('install-sbin-scripts=', None, "installation directory for sbin scripts")
    ]

    def initialize_options(self):
        _install.initialize_options(self)
        self.install_sbin_scripts = None

    def finalize_options(self):
        _install.finalize_options(self)
        if self.root is not None:
            self.change_roots('sbin_scripts')

    def finalize_unix(self):
        _install.finalize_unix(self)

        if self.install_base is not None or self.install_platbase is not None:
            if self.install_sbin_scripts is None:
                raise DistutilsOptionError, \
                      ("install-base or install-platbase supplied, but "
                      "installation scheme is incomplete")
            return



    def finalize_other(self):
        _install.finalize_other(self)
        # Nothing else to do here

    def expand_dirs(self):
        _install.expand_dirs(self)
        self._expand_attrs(['install_sbin_scripts'])

    # :TODO:
    # In run() add it to rejectdirs

class install_scripts(_install_scripts):

    user_options = _install_scripts.user_options + [
        ('install-sbin-dir=', 'd', "directory to install sbin scripts to")
    ]

    def initialize_options(self):
        _install_scripts.initialize_options(self)
        self.install_sbin_dir = None
        self.build_sbin_dir = None

    def finalize_options (self):
        _install_scripts.finalize_options(self)
        self.set_undefined_options(
            'build',
            ('build_sbin_scripts', 'build_sbin_dir'))
        self.set_undefined_options(
            'install',
            ('install_sbin_scripts', 'install_sbin_dir')
        )

    def run(self):
        _install_scripts.run(self)
        print self.build_sbin_dir
        print self.install_sbin_dir

        self.outfiles = self.copy_tree(self.build_sbin_dir, self.install_sbin_dir)
        if os.name == 'posix':
            # Set the executable bits (owner, group, and world) on
            # all the scripts we just installed.
            for file in self.get_outputs():
                if self.dry_run:
                    log.info("changing mode of %s", file)
                else:
                    mode = ((os.stat(file)[ST_MODE]) | 0555) & 07777
                    log.info("changing mode of %s to %o", file, mode)
                    os.chmod(file, mode)


class build(_build):

    user_options = _build_scripts.user_options + [
        ('build-sbin-scripts=', 'd', "directory to \"build\" (copy) to")
    ]

    def initialize_options(self):
        _build.initialize_options(self)
        self.build_sbin_scripts = None

    def finalize_options(self):
        _build.finalize_options(self)
        self.build_sbin_scripts = None

        if self.build_sbin_scripts is None:
            self.build_sbin_scripts = os.path.join(self.build_base,
                                              'sbin_scripts-' + sys.version[0:3])


class build_scripts(_build_scripts):

    user_options = _build_scripts.user_options + [
        ('build-sbin-dir=', 'd', "directory to \"build\" (copy) to")
    ]

    def initialize_options(self):
        _build_scripts.initialize_options(self)
        self.build_sbin_dir = None

    def finalize_options(self):
        _build_scripts.finalize_options(self)
        self.set_undefined_options(
            'build',
            ('build_sbin_scripts', 'build_sbin_dir')
        )
        self.sbin_scripts = self.distribution.sbin_scripts

    def copy_scripts (self):
        """Copy each script listed in 'self.scripts'; if it's marked as a
        Python script in the Unix way (first line matches 'first_line_re',
        ie. starts with "\#!" and contains "python"), then adjust the first
        line to refer to the current Python interpreter as we copy.
        """
        _build_scripts.copy_scripts(self)

        print "HERE"


        _sysconfig = __import__('sysconfig')
        self.mkpath(self.build_sbin_dir)
        outfiles = []
        for script in self.sbin_scripts:
            adjust = 0
            script = convert_path(script)
            outfile = os.path.join(self.build_sbin_dir, os.path.basename(script))
            outfiles.append(outfile)

            if not self.force and not newer(script, outfile):
                log.debug("not copying %s (up-to-date)", script)
                continue

            # Always open the file, but ignore failures in dry-run mode --
            # that way, we'll get accurate feedback if we can read the
            # script.
            try:
                f = open(script, "r")
            except IOError:
                if not self.dry_run:
                    raise
                f = None
            else:
                first_line = f.readline()
                if not first_line:
                    self.warn("%s is an empty file (skipping)" % script)
                    continue

                match = first_line_re.match(first_line)
                if match:
                    adjust = 1
                    post_interp = match.group(1) or ''

            if adjust:
                log.info("copying and adjusting %s -> %s", script,
                         self.build_dir)
                if not self.dry_run:
                    outf = open(outfile, "w")
                    if not _sysconfig.is_python_build():
                        outf.write("#!%s%s\n" %
                                   (self.executable,
                                    post_interp))
                    else:
                        outf.write("#!%s%s\n" %
                                   (os.path.join(
                            _sysconfig.get_config_var("BINDIR"),
                           "python%s%s" % (_sysconfig.get_config_var("VERSION"),
                                           _sysconfig.get_config_var("EXE"))),
                                    post_interp))
                    outf.writelines(f.readlines())
                    outf.close()
                if f:
                    f.close()
            else:
                if f:
                    f.close()
                self.copy_file(script, outfile)

        if os.name == 'posix':
            for file in outfiles:
                if self.dry_run:
                    log.info("changing mode of %s", file)
                else:
                    oldmode = os.stat(file)[ST_MODE] & 07777
                    newmode = (oldmode | 0555) & 07777
                    if newmode != oldmode:
                        log.info("changing mode of %s from %o to %o",
                                 file, oldmode, newmode)
                        os.chmod(file, newmode)

class Distribution(_Distribution):
    def __init__(self, *args, **kwargs):
        self.sbin_scripts = []
        _Distribution.__init__(self, *args, **kwargs)


def setup(*args, **kwargs):
    commands = {
        'install': install,
        'install_scripts': install_scripts,
        'build': build,
        'build_scripts': build_scripts
    }

    if 'cmdclass' in kwargs:
        for name in commands.keys():
            if name not in kwargs['cmdclass']:
                kwargs['cmdclass'][name] = commands[name]

    _setup(*args, **kwargs)


More information about the Distutils-SIG mailing list