commit 3a745a32f8323bbdf323e35eb4c7f0be84d9c6d6 Author: Damian Johnson atagar@torproject.org Date: Sun Jun 7 13:39:58 2015 -0700
Rewrite our setup.py
So much better. This borrows tricks from...
https://the-hitchhikers-guide-to-packaging.readthedocs.org/en/latest/specifi...
... to make a custom installer class rather than piling hacks on distutils as we previously did. In particular...
* Installer works once again (ongoing rewrite broke it).
* We can now install using python3. The codebase though isn't quite python3 compatible just yet.
* We now support --man-page and --sample-path arguments for customizing those locations.
* Test coverage for installation. This is similar to a test I recently wrote for Stem. --- install | 15 ---- nyx/__init__.py | 12 ++- nyx/starter.py | 12 +-- nyx/util/__init__.py | 2 +- setup.py | 225 ++++++++++++++++++++++++-------------------------- test/installation.py | 39 +++++++++ 6 files changed, 162 insertions(+), 143 deletions(-)
diff --git a/install b/install deleted file mode 100755 index be1f054..0000000 --- a/install +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh -python src/prereq.py - -if [ $? = 0 ]; then - python setup.py -q install - - # provide notice if we installed successfully - if [ $? = 0 ]; then - echo "installed to /usr/share/nyx" - fi - - # cleans up the automatically built temporary files - rm -rf ./build -fi - diff --git a/nyx/__init__.py b/nyx/__init__.py index c38b4f7..65e3c71 100644 --- a/nyx/__init__.py +++ b/nyx/__init__.py @@ -2,8 +2,12 @@ Tor curses monitoring application. """
-__version__ = '1.4.6_dev' +__version__ = '1.4.6-dev' __release_date__ = 'April 28, 2011' +__author__ = 'Damian Johnson' +__contact__ = 'atagar@torproject.org' +__url__ = 'http://www.atagar.com/arm/' +__license__ = 'GPLv3'
__all__ = [ 'arguments', @@ -33,15 +37,15 @@ def main(): else: advice = ', you can find it at https://stem.torproject.org/download.html'
- print 'nyx requires stem' + advice + print('nyx requires stem' + advice) elif exc.message == 'No module named curses': if distutils.spawn.find_executable('apt-get') is not None: advice = ", try running 'sudo apt-get install python-curses'" else: advice = '' # not sure what to do for other platforms
- print 'nyx requires curses' + advice + print('nyx requires curses' + advice) else: - print 'Unable to start nyx: %s' % exc + print('Unable to start nyx: %s' % exc)
sys.exit(1) diff --git a/nyx/starter.py b/nyx/starter.py index 87937ed..d541730 100644 --- a/nyx/starter.py +++ b/nyx/starter.py @@ -35,22 +35,22 @@ def main(config): args = nyx.arguments.parse(sys.argv[1:]) config.set('startup.events', args.logged_events) except ValueError as exc: - print exc + print(exc) sys.exit(1)
if args.print_help: - print nyx.arguments.get_help() + print(nyx.arguments.get_help()) sys.exit() elif args.print_version: - print nyx.arguments.get_version() + print(nyx.arguments.get_version()) sys.exit()
if args.debug_path is not None: try: _setup_debug_logging(args) - print msg('debug.saving_to_path', path = args.debug_path) + print(msg('debug.saving_to_path', path = args.debug_path)) except IOError as exc: - print msg('debug.unable_to_write_file', path = args.debug_path, error = exc.strerror) + print(msg('debug.unable_to_write_file', path = args.debug_path, error = exc.strerror)) sys.exit(1)
_load_user_nyxrc(args.config) @@ -91,7 +91,7 @@ def main(config): curses.wrapper(nyx.controller.start_nyx) except UnboundLocalError as exc: if os.environ['TERM'] != 'xterm': - print msg('setup.unknown_term', term = os.environ['TERM']) + print(msg('setup.unknown_term', term = os.environ['TERM'])) else: raise exc except KeyboardInterrupt: diff --git a/nyx/util/__init__.py b/nyx/util/__init__.py index 58e739c..13cb6db 100644 --- a/nyx/util/__init__.py +++ b/nyx/util/__init__.py @@ -28,7 +28,7 @@ TESTING = False try: uses_settings = stem.util.conf.uses_settings('nyx', os.path.join(BASE_DIR, 'config'), lazy_load = False) except IOError as exc: - print "Unable to load nyx's internal configurations: %s" % exc + print("Unable to load nyx's internal configurations: %s" % exc) sys.exit(1)
diff --git a/setup.py b/setup.py index c484ea1..a54ff89 100644 --- a/setup.py +++ b/setup.py @@ -1,125 +1,116 @@ #!/usr/bin/env python -import os -import sys +# Copyright 2015, Damian Johnson and The Tor Project +# See LICENSE for licensing information + import gzip -import tempfile -from nyx.version import VERSION +import os +import shutil +import stat + +import nyx + +from distutils import log from distutils.core import setup +from distutils.command.install import install
-def getResources(dst, sourceDir): - """ - Provides a list of tuples of the form... - [(destination, (file1, file2...)), ...] +DEFAULT_MAN_PAGE_PATH = '/usr/share/man/man1/nyx.1.gz' +DEFAULT_SAMPLE_PATH = '/usr/share/doc/nyx/nyxrc.sample' + + +def mkdir_for(path): + path_dir = os.path.dirname(path) + + if not os.path.exists(path_dir): + try: + os.makedirs(path_dir) + except OSError as exc: + raise OSError(None, "unable to make directory %s (%s)" % (path_dir, exc.strerror.lower())) + + +def install_man_page(source, dest): + if not os.path.exists(source): + raise OSError(None, "man page doesn't exist at '%s'" % source)
- for the given contents of the nyx directory (that's right, distutils isn't - smart enough to know how to copy directories). + mkdir_for(dest) + open_func = gzip.open if dest.endswith('.gz') else open + + with open(source, 'rb') as source_file: + with open_func(dest, 'wb') as dest_file: + dest_file.write(source_file.read()) + log.info("installed man page to '%s'" % dest) + + +def install_sample(source, dest): + if not os.path.exists(source): + raise OSError(None, "nyxrc sample doesn't exist at '%s'" % source) + + mkdir_for(dest) + shutil.copyfile(source, dest) + log.info("installed sample nyxrc to '%s'" % dest) + + +class NyxInstaller(install): """ + Nyx installer. This adds the following additional options... + + --man-page [path] + --sample-path [path] + + If the man page path ends in '.gz' it will be compressed. Empty paths such + as...
- results = [] - - for root, _, files in os.walk(os.path.join('nyx', sourceDir)): - if files: - fileListing = tuple([os.path.join(root, file) for file in files]) - results.append((os.path.join(dst, root[4:]), fileListing)) - - return results - -# Use 'tor-nyx' instead of 'nyx' in the path for the sample nyxrc if we're -# building for debian. - -isDebInstall = False -for arg in sys.argv: - if 'tor-nyx' in arg or 'release_deb' in arg: - isDebInstall = True - break - -docPath = '/usr/share/doc/%s' % ('tor-nyx' if isDebInstall else 'nyx') - -# Allow the docPath to be overridden via a '--docPath' argument. This is to -# support custom documentation locations on Gentoo, as discussed in: -# https://bugs.gentoo.org/349792 - -try: - docPathFlagIndex = sys.argv.index('--docPath') - if docPathFlagIndex < len(sys.argv) - 1: - docPath = sys.argv[docPathFlagIndex + 1] - - # remove the custom --docPath argument (otherwise the setup call will - # complain about them) - del sys.argv[docPathFlagIndex:docPathFlagIndex + 3] - else: - print 'No path provided for --docPath' - sys.exit(1) -except ValueError: pass # --docPath flag not found - -# Provides the configuration option to install to '/usr/share' rather than as a -# python module. Alternatives are to either provide this as an input argument -# (not an option for deb/rpm builds) or add a setup.cfg with: -# [install] -# install-purelib=/usr/share -# which would mean a bit more unnecessary clutter. - -manFilename = 'nyx/resoureces/nyx.1' -if 'install' in sys.argv: - sys.argv += ['--install-purelib', '/usr/share'] - - # Compresses the man page. This is a temporary file that we'll install. If - # something goes wrong then we'll print the issue and use the uncompressed man - # page instead. - - try: - manInputFile = open('nyx/resources/nyx.1', 'r') - manContents = manInputFile.read() - manInputFile.close() - - # temporary destination for the man page guarenteed to be unoccupied (to - # avoid conflicting with files that are already there) - tmpFilename = tempfile.mktemp('/nyx.1.gz') - - # make dir if the path doesn't already exist - baseDir = os.path.dirname(tmpFilename) - if not os.path.exists(baseDir): os.makedirs(baseDir) - - manOutputFile = gzip.open(tmpFilename, 'wb') - manOutputFile.write(manContents) - manOutputFile.close() - - # places in tmp rather than a relative path to avoid having this copy appear - # in the deb and rpm builds - manFilename = tmpFilename - except IOError, exc: - print 'Unable to compress man page: %s' % exc - -installPackages = ['nyx', 'nyx.cli', 'nyx.cli.graphing', 'nyx.cli.connections', 'nyx.cli.menu', 'nyx.util', 'nyx.stem'] - -setup(name='nyx', - version=VERSION, - description='Terminal tor status monitor', - license='GPL v3', - author='Damian Johnson', - author_email='atagar@torproject.org', - url='http://www.atagar.com/nyx/', - packages=installPackages, - package_dir={'nyx': 'nyx'}, - data_files=[('/usr/bin', ['run_nyx']), - ('/usr/share/man/man1', [manFilename]), - (docPath, ['nyxrc.sample']), - ('/usr/share/nyx/gui', ['nyx/gui/nyx.xml']), - ('/usr/share/nyx', ['nyx/settings.cfg', 'nyx/uninstall'])] + - getResources('/usr/share/nyx', 'resources'), - ) - -# Cleans up the temporary compressed man page. -if manFilename != 'nyx/resoureces/nyx.1' and os.path.isfile(manFilename): - if '-q' not in sys.argv: print 'Removing %s' % manFilename - os.remove(manFilename) - -# Removes the egg_info file. Apparently it is not optional during setup -# (hardcoded in distutils/command/install.py), nor are there any arguments to -# bypass its creation. The deb build removes this as part of its rules script. -eggPath = '/usr/share/nyx-%s.egg-info' % VERSION - -if not isDebInstall and os.path.isfile(eggPath): - if '-q' not in sys.argv: print 'Removing %s' % eggPath - os.remove(eggPath) + % python setup.py install --man-page '' + + ... will cause that resource to be omitted. + """
+ user_options = install.user_options + [ + ('man-page=', None, 'man page location (default: %s)' % DEFAULT_MAN_PAGE_PATH), + ('sample-path=', None, 'example nyxrc location (default: %s)' % DEFAULT_SAMPLE_PATH), + ] + + def initialize_options(self): + install.initialize_options(self) + self.man_page = DEFAULT_MAN_PAGE_PATH + self.sample_path = DEFAULT_SAMPLE_PATH + + def run(self): + install.run(self) + + # Install our bin script. We do this ourselves rather than with the setup() + # method's scripts argument because we want to call the script 'nyx' rather + # than 'run_nyx'. + + bin_dest = os.path.join(self.install_scripts, 'nyx') + mkdir_for(bin_dest) + shutil.copyfile('run_nyx', bin_dest) + mode = ((os.stat(bin_dest)[stat.ST_MODE]) | 0o555) & 0o7777 + os.chmod(bin_dest, mode) + log.info("installed bin script to '%s'" % bin_dest) + + if self.man_page: + install_man_page(os.path.join('nyx', 'resources', 'nyx.1'), self.man_page) + + if self.sample_path: + install_sample('nyxrc.sample', self.sample_path) + + +# installation requires us to be in our setup.py's directory + +setup_dir = os.path.dirname(os.path.join(os.getcwd(), __file__)) +os.chdir(setup_dir) + +setup( + name = 'nyx', + version = nyx.__version__, + description = 'Terminal status monitor for Tor https://www.torproject.org/', + license = nyx.__license__, + author = nyx.__author__, + author_email = nyx.__contact__, + url = nyx.__url__, + packages = ['nyx', 'nyx.connections', 'nyx.menu', 'nyx.util'], + keywords = 'tor onion controller', + install_requires = ['stem>=1.4.1'], + package_data = {'nyx': ['config/*', 'resources/*']}, + cmdclass = {'install': NyxInstaller}, +) diff --git a/test/installation.py b/test/installation.py new file mode 100644 index 0000000..e233d67 --- /dev/null +++ b/test/installation.py @@ -0,0 +1,39 @@ +import glob +import os +import shutil +import subprocess +import sys +import unittest + +import nyx +import stem.util.system + + +class TestInstallation(unittest.TestCase): + def test_installing_stem(self): + base_directory = os.path.sep.join(__file__.split(os.path.sep)[:-2]) + + if not os.path.exists(os.path.sep.join([base_directory, 'setup.py'])): + self.skipTest('(only for git checkout)') + + original_cwd = os.getcwd() + + try: + os.chdir(base_directory) + stem.util.system.call('python setup.py install --prefix /tmp/nyx_test --man-page /tmp/nyx_test/nyx.1.gz --sample-path /tmp/nyx_test/nyxrc.sample') + stem.util.system.call('python setup.py clean --all') # tidy up the build directory + site_packages_paths = glob.glob('/tmp/nyx_test/lib*/*/site-packages') + + if len(site_packages_paths) != 1: + self.fail('We should only have a single site-packages directory, but instead had: %s' % site_packages_paths) + + self.assertEqual(nyx.__version__, stem.util.system.call(['python', '-c', "import sys;sys.path.insert(0, '%s');import nyx;print(nyx.__version__)" % site_packages_paths[0]])[0]) + + process_path = sys.path + ['/tmp/nyx_test/lib/python2.7/site-packages'] + process = subprocess.Popen(['/tmp/nyx_test/bin/nyx', '--help'], stdout = subprocess.PIPE, env = {'PYTHONPATH': ':'.join(process_path)}) + stdout = process.communicate()[0] + + self.assertTrue(stdout.startswith('Usage nyx [OPTION]')) + finally: + shutil.rmtree('/tmp/nyx_test') + os.chdir(original_cwd)