#!/usr/bin/env python # # Copyright (C) 2007 Andrew Resch # Copyright (C) 2009 Damien Churchill # # This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with # the additional special exception to link portions of this program with the OpenSSL library. # See LICENSE for more details. # import glob import os import platform import sys from distutils.command.build import build as _build from distutils.command.clean import clean as _clean from distutils.command.install_data import install_data as _install_data from shutil import rmtree, which from setuptools import Command, find_packages, setup from setuptools.command.test import test as _test import msgfmt from version import get_version try: from sphinx.setup_command import BuildDoc except ImportError: class BuildDoc: pass def windows_check(): return platform.system() in ('Windows', 'Microsoft') def osx_check(): return platform.system() == 'Darwin' desktop_data = 'deluge/ui/data/share/applications/deluge.desktop' metainfo_data = 'deluge/ui/data/share/metainfo/deluge.metainfo.xml' # Variables for setuptools.setup _package_data = {} _exclude_package_data = {} _entry_points = {'console_scripts': [], 'gui_scripts': [], 'deluge.ui': []} _data_files = [] _version = get_version(prefix='deluge-', suffix='.dev0') class PyTest(_test): def initialize_options(self): _test.initialize_options(self) self.pytest_args = [] def finalize_options(self): _test.finalize_options(self) self.test_args = [] self.test_suite = True def run_tests(self): import pytest errcode = pytest.main(self.test_args) sys.exit(errcode) class CleanDocs(Command): description = 'Clean the documentation build and module rst files' user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): docs_build = 'docs/build' print(f'Deleting {docs_build}') try: rmtree(docs_build) except OSError: pass for module in glob.glob('docs/source/modules/deluge*.rst'): os.remove(module) class BuildWebUI(Command): description = 'Minify WebUI files' user_options = [] JS_DIR = os.path.join('deluge', 'ui', 'web', 'js') JS_SRC_DIRS = ('deluge-all', os.path.join('extjs', 'ext-extensions')) def initialize_options(self): pass def finalize_options(self): pass def run(self): js_basedir = os.path.join(os.path.dirname(__file__), self.JS_DIR) try: from minify_web_js import minify_js_dir import_error = '' except ImportError as err: import_error = err for js_src_dir in self.JS_SRC_DIRS: source_dir = os.path.join(js_basedir, js_src_dir) try: minify_js_dir(source_dir) except NameError: js_file = source_dir + '.js' if os.path.isfile(js_file): print( 'Unable to minify but found existing minified: {}'.format( js_file ) ) else: # Unable to minify and no existing minified file found so exiting. print('Import error: %s' % import_error) sys.exit(1) # Create the gettext.js file for translations. try: from gen_web_gettext import create_gettext_js except ImportError: pass else: deluge_all_path = os.path.join(js_basedir, self.JS_SRC_DIRS[0]) print('Creating WebUI translation file: %s/gettext.js' % deluge_all_path) create_gettext_js(deluge_all_path) class CleanWebUI(Command): description = 'Clean the documentation build and rst files' user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): js_basedir = os.path.join(os.path.dirname(__file__), BuildWebUI.JS_DIR) # Remove files generated by minify script. for js_src_dir in BuildWebUI.JS_SRC_DIRS: for file_type in ('.js', '-debug.js'): js_file = os.path.join(js_basedir, js_src_dir + file_type) print(f'Deleting {js_file}') try: os.remove(js_file) except OSError: pass # Remove generated gettext.js js_file = os.path.join(js_basedir, 'gettext.js') print(f'Deleting {js_file}') try: os.remove(js_file) except OSError: pass class BuildTranslations(Command): description = 'Compile .po files into .mo files & create .desktop file' user_options = [ ('build-lib', None, 'lib build folder'), ('develop', 'D', 'Compile translations in develop mode (deluge/i18n)'), ] boolean_options = ['develop'] def initialize_options(self): self.build_lib = None self.develop = False def finalize_options(self): self.set_undefined_options('build', ('build_lib', 'build_lib')) def run(self): po_dir = os.path.join(os.path.dirname(__file__), 'deluge', 'i18n') if self.develop: basedir = po_dir else: basedir = os.path.join(self.build_lib, 'deluge', 'i18n') intltool_merge = 'intltool-merge' if not windows_check() and which(intltool_merge): intltool_merge_opts = '--utf8 --quiet' for data_file in (desktop_data, metainfo_data): # creates the translated file from .in file. in_file = data_file + '.in' if 'xml' in data_file: intltool_merge_opts += ' --xml-style' elif 'desktop' in data_file: intltool_merge_opts += ' --desktop-style' print('Creating file: %s' % data_file) os.system( 'C_ALL=C ' + '%s ' * 5 % (intltool_merge, intltool_merge_opts, po_dir, in_file, data_file) ) print('Compiling po files from %s...' % po_dir) for path, names, filenames in os.walk(po_dir): for f in filenames: upto_date = False if f.endswith('.po'): lang = f[: len(f) - 3] src = os.path.join(path, f) dest_path = os.path.join(basedir, lang, 'LC_MESSAGES') dest = os.path.join(dest_path, 'deluge.mo') if not os.path.exists(dest_path): os.makedirs(dest_path) if not os.path.exists(dest): sys.stdout.write('%s, ' % lang) sys.stdout.flush() msgfmt.make(src, dest) else: src_mtime = os.stat(src)[8] dest_mtime = os.stat(dest)[8] if src_mtime > dest_mtime: sys.stdout.write('%s, ' % lang) sys.stdout.flush() msgfmt.make(src, dest) else: upto_date = True if upto_date: sys.stdout.write(' po files already up to date. ') sys.stdout.write('\b\b \nFinished compiling translation files. \n') class CleanTranslations(Command): description = 'Cleans translations files.' user_options = [ ('all', 'a', 'Remove all build output, not just temporary by-products') ] boolean_options = ['all'] def initialize_options(self): self.all = None def finalize_options(self): self.set_undefined_options('clean', ('all', 'all')) def run(self): for path in (desktop_data, metainfo_data): if os.path.isfile(path): print('Deleting %s' % path) os.remove(path) class BuildPlugins(Command): description = 'Build plugins into .eggs' user_options = [ ('install-dir=', None, 'develop install folder'), ('develop', 'D', 'Compile plugins in develop mode'), ] boolean_options = ['develop'] def initialize_options(self): self.install_dir = None self.develop = False def finalize_options(self): pass def run(self): # Build the plugin eggs plugin_path = 'deluge/plugins/*' for path in glob.glob(plugin_path): if os.path.exists(os.path.join(path, 'setup.py')): if self.develop and self.install_dir: os.system( 'cd ' + path + '&& ' + sys.executable + ' setup.py develop --install-dir=%s' % self.install_dir ) elif self.develop: os.system( 'cd ' + path + '&& ' + sys.executable + ' setup.py develop' ) else: os.system( 'cd ' + path + '&& ' + sys.executable + ' setup.py bdist_egg -d ..' ) class CleanPlugins(Command): description = 'Cleans the plugin folders' user_options = [ ('all', 'a', 'Remove all build output, not just temporary by-products') ] boolean_options = ['all'] def initialize_options(self): self.all = None def finalize_options(self): self.set_undefined_options('clean', ('all', 'all')) def run(self): print("Cleaning the plugin's folders...") plugin_path = 'deluge/plugins/*' for path in glob.glob(plugin_path): if os.path.exists(os.path.join(path, 'setup.py')): c = 'cd ' + path + ' && ' + sys.executable + ' setup.py clean' if self.all: c += ' -a' print("Calling '%s'" % c) os.system(c) # Delete the .eggs if path[-4:] == '.egg': print('Deleting egg file "%s"' % path) os.remove(path) # Delete the .egg-link if path[-9:] == '.egg-link': print('Deleting egg link "%s"' % path) os.remove(path) egg_info_dir_path = 'deluge/plugins/*/*.egg-info' for path in glob.glob(egg_info_dir_path): # Delete the .egg-info's directories if path[-9:] == '.egg-info': print('Deleting %s' % path) for fpath in os.listdir(path): os.remove(os.path.join(path, fpath)) os.removedirs(path) class EggInfoPlugins(Command): description = 'Create .egg-info directories for plugins' user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): # Build the plugin eggs plugin_path = 'deluge/plugins/*' for path in glob.glob(plugin_path): if os.path.exists(os.path.join(path, 'setup.py')): os.system('cd ' + path + '&& ' + sys.executable + ' setup.py egg_info') class Build(_build): sub_commands = [ ('build_webui', None), ('build_trans', None), ('build_plugins', None), ] + _build.sub_commands def run(self): # Run all sub-commands (at least those that need to be run). _build.run(self) try: from deluge._libtorrent import LT_VERSION print(f'Info: Found libtorrent ({LT_VERSION}) installed.') except ImportError as ex: print('Warning: libtorrent (libtorrent-rasterbar) not found: %s' % ex) class InstallData(_install_data): """Custom class to fix `setup install` copying data files to incorrect location. (Bug #1389)""" def finalize_options(self): self.install_dir = None self.set_undefined_options( 'install', ('install_data', 'install_dir'), ('root', 'root'), ('force', 'force'), ) def run(self): _install_data.run(self) class Clean(_clean): sub_commands = _clean.sub_commands + [ ('clean_plugins', None), ('clean_trans', None), ('clean_webui', None), ] def run(self): # Remove deluge egg-info. root_egg_info_dir_path = 'deluge*.egg-info' for path in glob.glob(root_egg_info_dir_path): print('Deleting %s' % path) for fpath in os.listdir(path): os.remove(os.path.join(path, fpath)) os.removedirs(path) # Run all sub-commands (at least those that need to be run) for cmd_name in self.get_sub_commands(): self.run_command(cmd_name) _clean.run(self) cmdclass = { 'build': Build, 'build_webui': BuildWebUI, 'build_trans': BuildTranslations, 'build_plugins': BuildPlugins, 'build_docs': BuildDoc, 'spellcheck_docs': BuildDoc, 'install_data': InstallData, 'clean_plugins': CleanPlugins, 'clean_trans': CleanTranslations, 'clean_docs': CleanDocs, 'clean_webui': CleanWebUI, 'clean': Clean, 'egg_info_plugins': EggInfoPlugins, 'test': PyTest, } if not windows_check() and not osx_check(): for icon_path in glob.glob('deluge/ui/data/icons/hicolor/*x*'): size = os.path.basename(icon_path) icons = glob.glob(os.path.join(icon_path, 'apps', 'deluge*.png')) _data_files.append((f'share/icons/hicolor/{size}/apps', icons)) _data_files.extend( [ ( 'share/icons/hicolor/scalable/apps', ['deluge/ui/data/icons/hicolor/scalable/apps/deluge.svg'], ), ('share/pixmaps', ['deluge/ui/data/pixmaps/deluge.png']), ( 'share/man/man1', [ 'docs/man/deluge.1', 'docs/man/deluged.1', 'docs/man/deluge-gtk.1', 'docs/man/deluge-web.1', 'docs/man/deluge-console.1', ], ), ] ) if os.path.isfile(desktop_data): _data_files.append(('share/applications', [desktop_data])) if os.path.isfile(metainfo_data): _data_files.append(('share/metainfo', [metainfo_data])) # Entry Points _entry_points['console_scripts'] = [ 'deluge-console = deluge.ui.console:start', ] # On Windows use gui_scripts to hide cmd popup (no effect on Linux/MacOS) _entry_points['gui_scripts'] = [ 'deluge = deluge.ui.ui_entry:start_ui', 'deluge-gtk = deluge.ui.gtk3:start', 'deluge-web = deluge.ui.web:start', 'deluged = deluge.core.daemon_entry:start_daemon', ] # Provide Windows 'debug' exes for stdin/stdout e.g. logging/errors if windows_check(): _entry_points['console_scripts'].extend( [ 'deluge-debug = deluge.ui.ui_entry:start_ui', 'deluge-web-debug = deluge.ui.web:start', 'deluged-debug = deluge.core.daemon_entry:start_daemon', ] ) _entry_points['deluge.ui'] = [ 'console = deluge.ui.console:Console', 'web = deluge.ui.web:Web', 'gtk = deluge.ui.gtk3:Gtk', ] _package_data['deluge'] = [ 'ui/data/pixmaps/*.png', 'ui/data/pixmaps/*.svg', 'ui/data/pixmaps/*.ico', 'ui/data/pixmaps/*.gif', 'ui/data/pixmaps/flags/*.png', 'plugins/*.egg', 'i18n/*/LC_MESSAGES/*.mo', ] _package_data['deluge.ui.web'] = [ 'index.html', 'css/*.css', 'icons/*.png', 'images/*.gif', 'images/*.png', 'js/*.js', 'js/extjs/*.js', 'render/*.html', 'themes/css/*.css', 'themes/images/*/*.gif', 'themes/images/*/*.png', 'themes/images/*/*/*.gif', 'themes/images/*/*/*.png', ] _package_data['deluge.ui.gtk3'] = ['glade/*.ui'] setup_requires = ['setuptools', 'wheel'] install_requires = [ 'twisted[tls]>=17.1', # Add pyasn1 for setuptools workaround: # https://github.com/pypa/setuptools/issues/1510 'pyasn1', 'rencode', 'pyopenssl', 'pyxdg', 'mako', 'setuptools', "pywin32; sys_platform == 'win32'", "certifi; sys_platform == 'win32'", 'zope.interface', 'prometheus-client>=0.21.0', ] extras_require = { 'all': [ 'setproctitle', 'pillow', 'chardet', 'ifaddr', ] } # Main setup setup( name='deluge', version=_version, fullname='Deluge BitTorrent Client', description='BitTorrent Client', author='Deluge Team', maintainer='Calum Lind', maintainer_email='calumlind+deluge@gmail.com', keywords='torrent bittorrent p2p fileshare filesharing', long_description=open('README.md').read(), long_description_content_type='text/markdown', url='https://deluge-torrent.org', project_urls={ 'GitHub (mirror)': 'https://github.com/deluge-torrent/deluge', 'Sourcecode': 'http://git.deluge-torrent.org/deluge', 'Issues': 'https://dev.deluge-torrent.org/report/1', 'Discussion': 'https://forum.deluge-torrent.org', 'Documentation': 'https://deluge.readthedocs.io', }, classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Console', 'Environment :: Web Environment', 'Environment :: X11 Applications :: GTK', 'Framework :: Twisted', 'Intended Audience :: End Users/Desktop', ( 'License :: OSI Approved :: ' 'GNU General Public License v3 or later (GPLv3+)' ), 'Programming Language :: Python', 'Operating System :: MacOS :: MacOS X', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX', 'Topic :: Internet', ], python_requires='>=3.6', license='GPLv3+', cmdclass=cmdclass, setup_requires=setup_requires, install_requires=install_requires, extras_require=extras_require, data_files=_data_files, package_data=_package_data, exclude_package_data=_exclude_package_data, packages=find_packages(exclude=['deluge.plugins.*', 'deluge.tests']), entry_points=_entry_points, )