diff --git a/Makefile b/Makefile index 6f55b340..4066a8f4 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,6 @@ MAINTAINER:=$(shell $(PYTHON) setup.py --maintainer) LAPPNAME:=$(shell echo $(APPNAME)|tr "[A-Z]" "[a-z]") ARCHIVE_SOURCE_EXT:=gz ARCHIVE_SOURCE:=$(APPNAME)-$(VERSION).tar.$(ARCHIVE_SOURCE_EXT) -ARCHIVE_WIN32:=$(APPNAME)-$(VERSION).exe GITUSER:=wummel GITREPO:=$(LAPPNAME) HOMEPAGE:=$(HOME)/public_html/$(LAPPNAME)-webpage.git @@ -111,14 +110,9 @@ tag: git tag upstream/$(VERSION) git push --tags origin upstream/$(VERSION) -upload: upload_source upload_binary - -upload_source: +upload: twine upload dist/$(ARCHIVE_SOURCE) dist/$(ARCHIVE_SOURCE).asc -upload_binary: - cp dist/$(ARCHIVE_WIN32) dist/$(ARCHIVE_WIN32).asc \ - $(HOMEPAGE)/dist homepage: # update metadata @@ -159,21 +153,6 @@ dist: locale MANIFEST chmod rm -f dist/$(ARCHIVE_SOURCE) $(PYTHON) setup.py sdist --formats=tar gzip --best dist/$(APPNAME)-$(VERSION).tar - [ ! -f ../$(ARCHIVE_WIN32) ] || cp ../$(ARCHIVE_WIN32) dist - -# Build OSX installer with py2app -app: distclean localbuild chmod - $(PYTHON) setup.py py2app $(PY2APPOPTS) - -# Build RPM installer with cx_Freeze -rpm: - $(MAKE) -C doc/html - $(MAKE) -C linkcheck/HtmlParser - $(PYTHON) setup.py bdist_rpm - -# Build portable Linux app -binary: distclean localbuild chmod - LINKCHECKER_FREEZE=1 $(PYTHON) setup.py bdist # The check programs used here are mostly local scripts on my private system. # So for other developers there is no need to execute this target. @@ -201,10 +180,6 @@ releasecheck: check @if egrep -i "xx\.|xxxx|\.xx" doc/changelog.txt > /dev/null; then \ echo "Could not release: edit doc/changelog.txt release date"; false; \ fi - @if [ ! -f ../$(ARCHIVE_WIN32) ]; then \ - echo "Missing WIN32 distribution archive at ../$(ARCHIVE_WIN32)"; \ - false; \ - fi $(PYTHON) setup.py check --restructuredtext sign: @@ -218,7 +193,7 @@ test: localbuild pyflakes: pyflakes $(PY_FILES_DIRS) 2>&1 | \ grep -v "local variable 'dummy' is assigned to but never used" | \ - grep -v -E "'(py2exe|py2app|PyQt4|biplist|setuptools|win32com|find_executable|parse_sitemap|parse_sitemapindex|parse_bookmark_data|parse_bookmark_file|wsgiref|pyftpdlib|linkchecker_rc)' imported but unused" | \ + grep -v -E "'(PyQt4|biplist|setuptools|win32com|find_executable|parse_sitemap|parse_sitemapindex|parse_bookmark_data|parse_bookmark_file|wsgiref|pyftpdlib|linkchecker_rc)' imported but unused" | \ grep -v "undefined name '_'" | \ grep -v "undefined name '_n'" | cat @@ -255,4 +230,4 @@ ide: .PHONY: test changelog gui count pyflakes ide login upload all clean distclean .PHONY: pep8 cleandeb locale localbuild deb diff dnsdiff sign .PHONY: filescheck update-copyright releasecheck check register announce -.PHONY: chmod dist app rpm release homepage +.PHONY: chmod dist release homepage diff --git a/setup.cfg b/setup.cfg index 147b3180..562ebf2d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,3 +10,6 @@ provides = linkchecker group = Applications/Internet install_script = install-rpm.sh python = python + +[bdist_wheel] +universal = 0 diff --git a/setup.py b/setup.py index b97646c3..e42369c7 100755 --- a/setup.py +++ b/setup.py @@ -19,11 +19,6 @@ Setup file for the distuils module. It includes the following features: -- py2exe support (including InnoScript installer generation) -- py2app support (including DMG package generation) -- cx_Freeze support -- Qt plugin installation for py2exe and py2app -- Microsoft Visual C++ DLL installation for py2exe - creation and installation of configuration files with installation data - automatic detection and usage of GNU99 standard for C compiler - automatic MANIFEST.in check @@ -50,22 +45,8 @@ try: except NameError: unicode = lambda x: x -# if a frozen Unix application should be built with cx_Freeze -do_freeze = int(os.environ.get('LINKCHECKER_FREEZE', '0')) - # import Distutils stuff -try: - # setuptools (which is needed by py2app) monkey-patches the - # distutils.core.Command class. - # So we need to import it before importing the distutils core - import setuptools -except ImportError: - # ignore when setuptools is not installed - setuptools = None -if do_freeze: - from cx_Freeze import setup, Executable -else: - from distutils.core import setup +from setuptools import setup from distutils.core import Extension from distutils.command.install_lib import install_lib from distutils.command.build_ext import build_ext @@ -74,29 +55,9 @@ from distutils.command.clean import clean from distutils.command.install_data import install_data from distutils.dir_util import remove_tree from distutils.file_util import write_file -from distutils.spawn import find_executable from distutils import util, log -try: - # py2exe monkey-patches the distutils.core.Distribution class - # So we need to import it before importing the Distribution class - import py2exe - has_py2exe = True -except ImportError: - # py2exe is not installed - has_py2exe = False -if do_freeze: - from cx_Freeze.dist import Distribution, build, install_exe - executables = [Executable("linkchecker"), Executable("linkchecker-gui")] -else: - from distutils.core import Distribution - from distutils.command.build import build - executables = None -try: - import py2app - has_py2app = True -except ImportError: - # py2app is not installed - has_py2app = False +from distutils.core import Distribution +from distutils.command.build import build # the application version AppVersion = "9.4" @@ -112,57 +73,6 @@ def get_long_description(): except: return Description -# Microsoft Visual C++ runtime version (tested with Python 2.7.2) -MSVCP90Version = '9.0.30729.6161' -MSVCP90Suffix = 'x-ww_31a54e43' -MSVCP90Token = '1fc8b3b9a1e18e3b' - -# basic includes for py2exe and py2app -py_includes = ['dns.rdtypes.IN.*', 'dns.rdtypes.ANY.*', - 'linkcheck.logger.*', -] -# basic excludes for py2exe and py2app -py_excludes = ['doctest', 'unittest', 'argcomplete', 'Tkinter', - 'PyQt4.QtDesigner', 'PyQt4.QtNetwork', 'PyQt4.QtOpenGL', - 'PyQt4.QtScript', 'PyQt4.QtTest', 'PyQt4.QtWebKit', 'PyQt4.QtXml', - 'PyQt4.phonon', -] -# py2exe options for Windows packaging -py2exe_options = dict( - packages=["encodings"], - excludes=py_excludes + ['win32com.gen_py'], - # silence py2exe error about not finding msvcp90.dll - dll_excludes=['MSVCP90.dll'], - # add sip so that PyQt4 works - # add PyQt4.QtSql so that sqlite needed by QHelpCollection works - includes=py_includes + ["sip", "PyQt4.QtSql"], - compressed=1, - optimize=2, -) -# py2app options for OSX packaging -py2app_options = dict( - includes=py_includes + ['sip', 'PyQt4', - 'PyQt4.QtCore', 'PyQt4.QtGui', 'PyQt4.QtSql'], - excludes=py_excludes, - strip=True, - optimize=2, - iconfile='doc/html/favicon.icns', - plist={ - 'CFBundleIdentifier': 'org.pythonmac.%s' % AppName, - 'CFBundleIconFile': 'favicon.icns', - }, - argv_emulation=True, -) -# cx_Freeze for Linux RPM packaging -cx_includes = [x[:-2] for x in py_includes] -cxfreeze_options = dict( - packages=["encodings"], - excludes=py_excludes, - includes=cx_includes + ['sip', 'PyQt4', - 'PyQt4.QtCore', 'PyQt4.QtGui', 'PyQt4.QtSql'], -) - - def normpath (path): """Norm a path name to platform specific notation.""" return os.path.normpath(path) @@ -179,21 +89,6 @@ def cnormpath (path): return path -def get_nt_platform_vars (): - """Return program file path and architecture for NT systems.""" - platform = util.get_platform() - if platform == "win-amd64": - # the Visual C++ runtime files are installed in the x86 directory - progvar = "%ProgramFiles(x86)%" - architecture = "amd64" - elif platform == "win32": - progvar = "%ProgramFiles%" - architecture = "x86" - else: - raise ValueError("Unsupported platform %r" % platform) - return os.path.expandvars(progvar), architecture - - release_ro = re.compile(r"\(released (.+)\)") def get_release_date (): """Parse and return relase date as string from doc/changelog.txt.""" @@ -213,129 +108,6 @@ def get_portable(): return os.environ.get('LINKCHECKER_PORTABLE', '0') -def get_qt_plugin_dir_win (): - """Get Qt plugin dir on Windows systems.""" - import PyQt4 - return os.path.join(os.path.dirname(PyQt4.__file__), "plugins") - - -def get_qt_plugin_dir_osx (): - """Get Qt plugin dir on OSX systems.""" - # note: works on Qt installed with homebrew - qtbindir = os.path.dirname(os.path.realpath(find_executable("qmake"))) - return os.path.join(os.path.dirname(qtbindir), "plugins") - - -def add_qt_plugin_file (files, plugin_dir, dirname, filename): - """Add one Qt plugin file to list of data files.""" - files.append((dirname, [os.path.join(plugin_dir, dirname, filename)])) - - -def add_qt_plugin_files (files): - """Add needed Qt plugins to list of data files. Filename prefix and - suffix are different for Windows and OSX.""" - if os.name == 'nt': - plugin_dir = get_qt_plugin_dir_win() - args = ("", "4.dll") - elif sys.platform == 'darwin': - plugin_dir = get_qt_plugin_dir_osx() - args = ("lib", ".dylib") - else: - raise ValueError("unsupported qt plugin platform") - # Copy needed sqlite plugin files to distribution directory. - add_qt_plugin_file(files, plugin_dir, "sqldrivers", "%sqsqlite%s" % args) - # Copy needed gif image plugin files to distribution directory. - add_qt_plugin_file(files, plugin_dir, "imageformats", "%sqgif%s" % args) - - -def fix_qt_plugins_py2app (dist_dir): - """Fix Qt plugin files installed in data_dir by moving them to - app_dir/Plugins and change the install_name.""" - app_dir = os.path.join(dist_dir, '%s.app' % AppName, 'Contents') - plugin_dir = os.path.join(app_dir, 'Plugins') - data_dir = os.path.join(app_dir, 'Resources') - qt_lib_dir = os.path.join(os.path.dirname(get_qt_plugin_dir_osx()), 'lib') - # make target plugin directory - os.mkdir(plugin_dir) - qt_plugins = ('sqldrivers', 'imageformats') - qt_modules = ('QtCore', 'QtGui', 'QtSql') - for plugin in qt_plugins: - target_dir = os.path.join(plugin_dir, plugin) - # move libraries - os.rename(os.path.join(data_dir, plugin), target_dir) - # fix libraries - for library in glob.glob("%s/*.dylib" % target_dir): - for module in qt_modules: - libpath = "%s.framework/Versions/4/%s" % (module, module) - oldpath = os.path.join(qt_lib_dir, libpath) - newpath = '@executable_path/../Frameworks/%s' % libpath - args = ['install_name_tool', '-change', oldpath, newpath, library] - subprocess.check_call(args) - - -def generate_dmg_image (dist_dir): - """Generate .dmg image.""" - imgPath = os.path.join(dist_dir, "%s-%s.dmg" % (AppName, AppVersion)) - tmpImgPath = os.path.join(dist_dir, "%s.tmp.dmg" % AppName) - print("*** generating temporary DMG image ***") - args = ['hdiutil', 'create', '-srcfolder', dist_dir, '-fs', 'HFSX', - '-volname', AppName, '-format', 'UDZO', tmpImgPath] - subprocess.check_call(args) - print("*** generating final DMG image ***") - args = ['hdiutil', 'convert', tmpImgPath, '-format', 'UDZO', - '-imagekey', 'zlib-level=9', '-o', imgPath] - subprocess.check_call(args) - os.remove(tmpImgPath) - - -def sign_the_code (dist_dir): - """Sign the OSX application code.""" - app_dir = os.path.join(dist_dir, "%s.app" % AppName) - args = ['codesign', '-s', myname, '-v', app_dir] - print("*** signing the application code ***") - try: - subprocess.check_call(args) - except subprocess.CalledProcessError as msg: - print("WARN: codesigning failed", msg) - - -def add_msvc_files (files): - """Add needed MSVC++ runtime files. Only Version 9.0.21022.8 is tested - and can be downloaded here: - http://www.microsoft.com/en-us/download/details.aspx?id=5582 - """ - prog_dir, architecture = get_nt_platform_vars() - dirname = "Microsoft.VC90.CRT" - version = "%s_%s_%s" % (MSVCP90Token, MSVCP90Version, MSVCP90Suffix) - args = (architecture, dirname, version) - path = r'C:\Windows\WinSxS\%s_%s_%s\*.*' % args - files.append((dirname, glob.glob(path))) - # Copy the manifest file into the build directory and rename it - # because it must have the same name as the directory. - path = r'C:\Windows\WinSxS\Manifests\%s_%s_%s.manifest' % args - target = os.path.join(os.getcwd(), 'build', '%s.manifest' % dirname) - shutil.copy(path, target) - files.append((dirname, [target])) - - -def add_requests_cert_file(files): - """Add Python requests .pem file for installers.""" - import requests - filename = os.path.join(os.path.dirname(requests.__file__), 'cacert.pem') - dirname = 'share/linkchecker' - files.append((dirname, [filename])) - - -def insert_dns_path(): - """Let py2exe, py2app and cx_Freeze find the dns package.""" - lib_dir = "lib.%s-%s" % (util.get_platform(), sys.version[0:3]) - if hasattr(sys, 'gettotalrefcount'): - lib_dir += '-pydebug' - dnspath = os.path.abspath(os.path.join('build', lib_dir, 'linkcheck_dns')) - if dnspath not in sys.path: - sys.path.insert(0, dnspath) - - class MyInstallLib (install_lib, object): """Custom library installation.""" @@ -418,33 +190,6 @@ class MyInstallData (install_data, object): os.chmod(path, mode) -# Microsoft application manifest for linkchecker-gui.exe; see also -# http://msdn.microsoft.com/en-us/library/aa374191%28VS.85%29.aspx -app_manifest = """ - - - - - - - - - - -""" % dict(appversion=AppVersion, appname=AppName, - msvcrtversion=MSVCP90Version, msvcrttoken=MSVCP90Token) - class MyDistribution (Distribution, object): """Custom distribution class generating config file.""" @@ -452,11 +197,6 @@ class MyDistribution (Distribution, object): """Set console and windows scripts.""" super(MyDistribution, self).__init__(attrs) self.console = ['linkchecker'] - self.windows = [{ - "script": "linkchecker-gui", - "icon_resources": [(1, "doc/html/favicon.ico")], - "other_resources": [(24, 1, app_manifest)], - }] def run_commands (self): """Generate config file and run commands.""" @@ -650,7 +390,7 @@ data_files = [ ] for (src, dst) in list_message_files(AppName): - data_files.append((src, dst)) + data_files.append((dst, [src])) if os.name == 'posix': data_files.append(('share/man/man1', ['doc/en/linkchecker.1', 'doc/en/linkchecker-gui.1'])) @@ -664,198 +404,6 @@ if os.name == 'posix': 'doc/examples/check_urls.sh'])) data_files.append(('share/applications', ['doc/linkchecker.desktop'])) data_files.append(('share/applications', ['doc/linkchecker-gui.desktop'])) -if 'py2app' in sys.argv[1:]: - if not has_py2app: - raise SystemExit("py2app module could not be imported.") - # add Qt plugins which are later fixed by fix_qt_plugins_py2app() - add_qt_plugin_files(data_files) - # needed for Qt to load the plugins - data_files.append(('', ['osx/qt.conf'])) - add_requests_cert_file(data_files) - insert_dns_path() -elif 'py2exe' in sys.argv[1:]: - if not has_py2exe: - raise SystemExit("py2exe module could not be imported") - add_qt_plugin_files(data_files) - add_msvc_files(data_files) - add_requests_cert_file(data_files) - insert_dns_path() -elif do_freeze: - class MyInstallExe (install_exe, object): - """Install cx_Freeze executables.""" - def run (self): - """Add generated configuration to output files.""" - super(MyInstallExe, self).run() - cmd_obj = self.distribution.get_command_obj("install_lib") - cmd_obj.ensure_finalized() - self.outfiles.append(cmd_obj.get_conf_output()+"c") - insert_dns_path() - - -class InnoScript: - """Class to generate INNO script.""" - - def __init__(self, lib_dir, dist_dir, windows_exe_files=[], - console_exe_files=[], service_exe_files=[], - comserver_files=[], lib_files=[]): - """Store INNO script infos.""" - self.lib_dir = lib_dir - self.dist_dir = dist_dir - if not self.dist_dir[-1] in "\\/": - self.dist_dir += "\\" - self.name = AppName - self.version = AppVersion - self.windows_exe_files = [self.chop(p) for p in windows_exe_files] - self.console_exe_files = [self.chop(p) for p in console_exe_files] - self.service_exe_files = [self.chop(p) for p in service_exe_files] - self.comserver_files = [self.chop(p) for p in comserver_files] - self.lib_files = [self.chop(p) for p in lib_files] - self.icon = os.path.abspath(r'doc\html\favicon.ico') - - def chop(self, pathname): - """Remove distribution directory from path name.""" - assert pathname.startswith(self.dist_dir) - return pathname[len(self.dist_dir):] - - def create(self, pathname=r"dist\omt.iss"): - """Create Inno script.""" - self.pathname = pathname - self.distfilebase = "%s-%s" % (self.name, self.version) - self.distfile = self.distfilebase + ".exe" - with codecs.open(self.pathname, "w", 'utf-8-sig', 'strict') as fd: - self.write_inno_script(fd) - - def write_inno_script (self, fd): - """Write Inno script contents.""" - print("; WARNING: This script has been created by py2exe. Changes to this script", file=fd) - print("; will be overwritten the next time py2exe is run!", file=fd) - print("[Setup]", file=fd) - print("AppName=%s" % self.name, file=fd) - print("AppVerName=%s %s" % (self.name, self.version), file=fd) - print(r"DefaultDirName={pf}\%s" % self.name, file=fd) - print("DefaultGroupName=%s" % self.name, file=fd) - print("OutputBaseFilename=%s" % self.distfilebase, file=fd) - print("OutputDir=..", file=fd) - print("SetupIconFile=%s" % self.icon, file=fd) - print("UninstallDisplayIcon=%s" % self.icon, file=fd) - print(file=fd) - # Customize some messages - print("[Messages]", file=fd) - print("ConfirmUninstall=Are you sure you want to remove %1? Note that user-specific configuration files of %1 are not removed.", file=fd) - print("BeveledLabel=DON'T PANIC", file=fd) - print(file=fd) - # List of source files - files = self.windows_exe_files + \ - self.console_exe_files + \ - self.service_exe_files + \ - self.comserver_files + \ - self.lib_files - print('[Files]', file=fd) - for path in files: - print(r'Source: "%s"; DestDir: "{app}\%s"; Flags: ignoreversion' % (path, os.path.dirname(path)), file=fd) - print(file=fd) - # Set icon filename - print('[Icons]', file=fd) - for path in self.windows_exe_files: - print(r'Name: "{group}\%s"; Filename: "{app}\%s"' % \ - (self.name, path), file=fd) - print(r'Name: "{group}\Uninstall %s"; Filename: "{uninstallexe}"' % self.name, file=fd) - print(file=fd) - # Uninstall registry keys - print('[Registry]', file=fd) - print(r'Root: HKCU; Subkey: "Software\Bastian\LinkChecker"; Flags: uninsdeletekey', file=fd) - print(file=fd) - # Uninstall optional log files - print('[UninstallDelete]', file=fd) - for path in (self.windows_exe_files + self.console_exe_files): - exename = os.path.basename(path) - print(r'Type: files; Name: "{pf}\%s\%s.log"' % (self.name, exename), file=fd) - print(file=fd) - - def compile (self): - """Compile Inno script with iscc.exe.""" - progpath = get_nt_platform_vars()[0] - cmd = r'%s\Inno Setup 5\iscc.exe' % progpath - subprocess.check_call([cmd, self.pathname]) - - def sign (self): - """Sign InnoSetup installer with local self-signed certificate.""" - print("*** signing the inno setup installer ***") - pfxfile = r'windows\linkchecker.pfx' - if os.path.isfile(pfxfile): - path = get_windows_sdk_path() - signtool = os.path.join(path, "bin", "signtool.exe") - if os.path.isfile(signtool): - cmd = [signtool, 'sign', '/f', pfxfile, self.distfile] - subprocess.check_call(cmd) - else: - print("No signed installer: signtool.exe not found.") - else: - print("No signed installer: certificate %s not found." % pfxfile) - -def get_windows_sdk_path(): - """Return path of Microsoft Windows SDK installation, or None if - not found.""" - try: - import _winreg as winreg - except ImportError: - import winreg - sub_key = r"Software\Microsoft\Microsoft SDKs\Windows" - with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, sub_key) as key: - name = "CurrentInstallFolder" - return winreg.QueryValueEx(key, name)[0] - return None - -try: - from py2exe.build_exe import py2exe as py2exe_build - - class MyPy2exe (py2exe_build): - """First builds the exe file(s), then creates a Windows installer. - Needs InnoSetup to be installed.""" - - def run (self): - """Generate py2exe installer.""" - # First, let py2exe do it's work. - py2exe_build.run(self) - print("*** preparing the inno setup script ***") - lib_dir = self.lib_dir - dist_dir = self.dist_dir - # create the Installer, using the files py2exe has created. - script = InnoScript(lib_dir, dist_dir, self.windows_exe_files, - self.console_exe_files, self.service_exe_files, - self.comserver_files, self.lib_files) - print("*** creating the inno setup script ***") - script.create() - print("*** compiling the inno setup script ***") - script.compile() - script.sign() -except ImportError: - class MyPy2exe: - """Dummy py2exe class.""" - pass - - -try: - from py2app.build_app import py2app as py2app_build - - class MyPy2app (py2app_build): - """First builds the app file(s), then creates a DMG installer. - Needs hdiutil to be installed.""" - - def run (self): - """Generate py2app installer.""" - # First, let py2app do it's work. - py2app_build.run(self) - # Fix install names for Qt plugin libraries. - fix_qt_plugins_py2app(self.dist_dir) - sign_the_code(self.dist_dir) - generate_dmg_image(self.dist_dir) - -except ImportError: - class MyPy2app: - """Dummy py2app class.""" - pass - args = dict( name = AppName, @@ -877,8 +425,6 @@ args = dict( 'build': MyBuild, 'clean': MyClean, 'sdist': MySdist, - 'py2exe': MyPy2exe, - 'py2app': MyPy2app, }, package_dir = { 'linkcheck_dns.dns': 'third_party/dnspython/dns', @@ -934,9 +480,6 @@ args = dict( 'Programming Language :: C', ], options = { - "py2exe": py2exe_options, - "py2app": py2app_options, - "build_exe": cxfreeze_options, }, # Requirements, usable with setuptools or the new Python packaging module. # Commented out since they are untested and not officially supported. @@ -948,13 +491,4 @@ args = dict( # "Memory debugging": ['meliae'], # https://launchpad.net/meliae #} ) -if sys.platform == 'darwin': - args["app"] = ['linkchecker-gui'] -if executables: - args["executables"] = executables - args["cmdclass"]["install_exe"] = MyInstallExe -if setuptools is not None: - args['install_requires'] = [ - 'requests >= 2.2.0', - ] setup(**args)