linkchecker/setup.py
2009-01-17 13:16:50 +00:00

590 lines
22 KiB
Python
Executable file

#!/usr/bin/python
# -*- coding: iso-8859-1 -*-
# Copyright (C) 2000-2009 Bastian Kleineidam
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
"""
Setup file for the distuils module.
"""
import sys
if not (hasattr(sys, 'version_info') or
sys.version_info < (2, 5, 0, 'final', 0)):
raise SystemExit("This program requires Python 2.5 or later.")
import os
import subprocess
import platform
import stat
import glob
# Use setuptools to support eggs. Note that this conflicts with py2exe.
# The setuptools are not officially supported.
#try:
# from setuptools import setup
# from setuptools.command.bdist_wininst import bdist_wininst
# from setuptools.command.install_lib import install_lib
# from setuptools.command.build_ext import build_ext
# from setuptools.command.sdist import sdist
# from distutils.command.sdist import sdist as _sdist
# sdist.user_options = _sdist.user_options + sdist.user_options
# from setuptools.extension import Extension
# from setuptools.dist import Distribution
#except ImportError:
# use distutils
from distutils.core import setup, Extension, Distribution
from distutils.command.bdist_wininst import bdist_wininst
from distutils.command.install_lib import install_lib
from distutils.command.build_ext import build_ext
from distutils.command.sdist import sdist
import distutils.command
from distutils.command.clean import clean
from distutils.command.build import build
from distutils.command.install_data import install_data
from distutils.spawn import find_executable
from distutils.dir_util import remove_tree
from distutils.file_util import write_file
from distutils.sysconfig import get_python_version
from distutils.errors import DistutilsPlatformError
from distutils import util, log
try:
# Note that py2exe monkey-patches the distutils.core.Distribution class
import py2exe
except ImportError:
# ignore when py2exe is not installed
pass
py2exe_options = dict(
packages=["encodings"],
excludes=['doctest', 'unittest', 'optcomplete', 'Tkinter'],
includes=["sip"],
compressed=1,
optimize=2,
)
# cross compile config
cc = os.environ.get("CC")
# directory with cross compiled (for win32) python
win_python_dir = "/home/calvin/src/python23-maint-cvs/dist/src/"
# if we are compiling for or under windows
win_compiling = (os.name == 'nt') or (cc is not None and "mingw32" in cc)
def normpath (path):
"""Norm a path name to platform specific notation."""
return os.path.normpath(path)
def cnormpath (path):
"""Norm a path name to platform specific notation, but honoring
the win_compiling flag."""
path = normpath(path)
if win_compiling:
# replace slashes with backslashes
path = path.replace("/", "\\")
if not os.path.isabs(path):
path= normpath(os.path.join(sys.prefix, path))
return path
class MyInstallLib (install_lib, object):
def install (self):
"""Install the generated config file."""
outs = super(MyInstallLib, self).install()
infile = self.create_conf_file()
outfile = os.path.join(self.install_dir, os.path.basename(infile))
self.copy_file(infile, outfile)
outs.append(outfile)
return outs
def create_conf_file (self):
cmd_obj = self.distribution.get_command_obj("install")
cmd_obj.ensure_finalized()
# we have to write a configuration file because we need the
# <install_data> directory (and other stuff like author, url, ...)
# all paths are made absolute by cnormpath()
data = []
for d in ['purelib', 'platlib', 'lib', 'headers', 'scripts', 'data']:
attr = 'install_%s' % d
if cmd_obj.root:
# cut off root path prefix
cutoff = len(cmd_obj.root)
# don't strip the path separator
if cmd_obj.root.endswith(os.sep):
cutoff -= 1
val = getattr(cmd_obj, attr)[cutoff:]
else:
val = getattr(cmd_obj, attr)
if attr == 'install_data':
cdir = os.path.join(val, "share", "linkchecker")
data.append('config_dir = %r' % cnormpath(cdir))
elif attr == 'install_lib':
if cmd_obj.root:
_drive, tail = os.path.splitdrive(val)
if tail.startswith(os.sep):
tail = tail[1:]
self.install_lib = os.path.join(cmd_obj.root, tail)
else:
self.install_lib = val
data.append("%s = %r" % (attr, cnormpath(val)))
self.distribution.create_conf_file(data, directory=self.install_lib)
return self.distribution.get_conf_filename(self.install_lib)
def get_outputs (self):
"""Add the generated config file to the list of outputs."""
outs = super(MyInstallLib, self).get_outputs()
outs.append(self.distribution.get_conf_filename(self.install_lib))
return outs
class MyInstallData (install_data, object):
"""My own data installer to handle permissions."""
def run (self):
"""Adjust permissions on POSIX systems."""
super(MyInstallData, self).run()
if os.name == 'posix' and not self.dry_run:
# Make the data files we just installed world-readable,
# and the directories world-executable as well.
for path in self.get_outputs():
mode = os.stat(path)[stat.ST_MODE]
if stat.S_ISDIR(mode):
mode |= 011
mode |= 044
os.chmod(path, mode)
class MyDistribution (distutils.core.Distribution, object):
"""Custom distribution class generating config file."""
def __init__ (self, attrs):
super(MyDistribution, self).__init__(attrs)
self.console = ['linkchecker']
self.windows = ['linkchecker-gui']
def run_commands (self):
"""Generate config file and run commands."""
cwd = os.getcwd()
data = []
data.append('config_dir = %r' % os.path.join(cwd, "config"))
data.append("install_data = %r" % cwd)
data.append("install_scripts = %r" % cwd)
self.create_conf_file(data)
super(MyDistribution, self).run_commands()
def get_conf_filename (self, directory):
"""Get name for config file."""
return os.path.join(directory, "_%s_configdata.py" % self.get_name())
def create_conf_file (self, data, directory=None):
"""Create local config file from given data (list of lines) in
the directory (or current directory if not given)."""
data.insert(0, "# this file is automatically created by setup.py")
data.insert(0, "# -*- coding: iso-8859-1 -*-")
if directory is None:
directory = os.getcwd()
filename = self.get_conf_filename(directory)
# add metadata
metanames = ("name", "version", "author", "author_email",
"maintainer", "maintainer_email", "url",
"license", "description", "long_description",
"keywords", "platforms", "fullname", "contact",
"contact_email", "fullname")
for name in metanames:
method = "get_" + name
val = getattr(self.metadata, method)()
if isinstance(val, str):
val = unicode(val)
cmd = "%s = %r" % (name, val)
data.append(cmd)
# write the config file
data.append('appname = "LinkChecker"')
util.execute(write_file, (filename, data),
"creating %s" % filename, self.verbose>=1, self.dry_run)
class MyBdistWininst (bdist_wininst, object):
"""Custom bdist_wininst command supporting cross compilation."""
def run (self):
if (not win_compiling and
(self.distribution.has_ext_modules() or
self.distribution.has_c_libraries())):
raise DistutilsPlatformError \
("distribution contains extensions and/or C libraries; "
"must be compiled on a Windows 32 platform")
if not self.skip_build:
self.run_command('build')
install = self.reinitialize_command('install', reinit_subcommands=1)
install.root = self.bdist_dir
install.skip_build = self.skip_build
install.warn_dir = 0
install_lib = self.reinitialize_command('install_lib')
# we do not want to include pyc or pyo files
install_lib.compile = 0
install_lib.optimize = 0
# If we are building an installer for a Python version other
# than the one we are currently running, then we need to ensure
# our build_lib reflects the other Python version rather than ours.
# Note that for target_version!=sys.version, we must have skipped the
# build step, so there is no issue with enforcing the build of this
# version.
target_version = self.target_version
if not target_version:
assert self.skip_build, "Should have already checked this"
target_version = sys.version[0:3]
plat_specifier = ".%s-%s" % (util.get_platform(), target_version)
build = self.get_finalized_command('build')
build.build_lib = os.path.join(build.build_base,
'lib' + plat_specifier)
# Use a custom scheme for the zip-file, because we have to decide
# at installation time which scheme to use.
for key in ('purelib', 'platlib', 'headers', 'scripts', 'data'):
value = key.upper()
if key == 'headers':
value += '/Include/$dist_name'
setattr(install,
'install_' + key,
value)
log.info("installing to %s", self.bdist_dir)
install.ensure_finalized()
# avoid warning of 'install_lib' about installing
# into a directory not in sys.path
oldpath = sys.path
sys.path.insert(0, os.path.join(self.bdist_dir, 'PURELIB'))
install.run()
sys.path = oldpath
# And make an archive relative to the root of the
# pseudo-installation tree.
from tempfile import mktemp
archive_basename = mktemp()
fullname = self.distribution.get_fullname()
arcname = self.make_archive(archive_basename, "zip",
root_dir=self.bdist_dir)
# create an exe containing the zip-file
self.create_exe(arcname, fullname, self.bitmap)
# remove the zip-file again
log.debug("removing temporary file '%s'", arcname)
os.remove(arcname)
if not self.keep_temp:
remove_tree(self.bdist_dir, dry_run=self.dry_run)
def get_exe_bytes (self):
if win_compiling:
# wininst-X.Y.exe is in the same directory as bdist_wininst
directory = os.path.dirname(distutils.command.__file__)
filename = os.path.join(directory, "wininst-7.1.exe")
return open(filename, "rb").read()
return super(MyBdistWininst, self).get_exe_bytes()
def cc_supports_option (cc, option):
"""Check if the given C compiler supports the given option.
@return: True if the compiler supports the option, else False
@rtype: bool
"""
prog = "int main(){}\n"
pipe = subprocess.Popen([cc[0], "-E", option, "-"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, close_fds=True)
pipe.communicate(input=prog)
if os.WIFEXITED(pipe.returncode):
return os.WEXITSTATUS(pipe.returncode) == 0
return False
def cc_remove_option (compiler, option):
for optlist in (compiler.compiler, compiler.compiler_so):
if option in optlist:
optlist.remove(option)
class MyBuildExt (build_ext, object):
"""Custom build extension command."""
def build_extensions (self):
"""Add -std=gnu99 to build options if supported.
And compress extension libraries."""
# For gcc >= 3 we can add -std=gnu99 to get rid of warnings.
extra = []
if self.compiler.compiler_type == 'unix':
option = "-std=gnu99"
if cc_supports_option(self.compiler.compiler, option):
extra.append(option)
if platform.machine() == 'm68k':
# work around ICE on m68k machines in gcc 4.0.1
cc_remove_option(self.compiler, "-O3")
# First, sanity-check the 'extensions' list
self.check_extensions_list(self.extensions)
for ext in self.extensions:
for opt in extra:
if opt not in ext.extra_compile_args:
ext.extra_compile_args.append(opt)
self.build_extension(ext)
self.compress_extensions()
def compress_extensions (self):
"""Run UPX compression over built extension libraries."""
# currently upx supports only .dll files
if os.name != 'nt':
return
upx = find_executable("upx")
if upx is None:
# upx not found
return
for filename in self.get_outputs():
compress_library(upx, filename)
def compress_library (upx, filename):
"""Compresses a dynamic library file with upx (currently only .dll
files are supported)."""
log.info("upx-compressing %s", filename)
os.system('%s -q --best "%s"' % (upx, filename))
def list_message_files (package, suffix=".po"):
"""Return list of all found message files and their installation paths."""
_files = glob.glob("po/*" + suffix)
_list = []
for _file in _files:
# basename (without extension) is a locale name
_locale = os.path.splitext(os.path.basename(_file))[0]
_list.append((_file, os.path.join(
"share", "locale", _locale, "LC_MESSAGES", "%s.mo" % package)))
return _list
def check_manifest ():
"""Snatched from roundup.sf.net.
Check that the files listed in the MANIFEST are present when the
source is unpacked."""
try:
f = open('MANIFEST')
except Exception:
print '\n*** SOURCE WARNING: The MANIFEST file is missing!'
return
try:
manifest = [l.strip() for l in f.readlines()]
finally:
f.close()
err = [line for line in manifest if not os.path.exists(line)]
if err:
n = len(manifest)
print '\n*** SOURCE WARNING: There are files missing (%d/%d found)!'%(
n-len(err), n)
print 'Missing:', '\nMissing: '.join(err)
class MyBuild (build, object):
"""Custom build command."""
def build_message_files (self):
"""For each po/*.po, build .mo file in target locale directory."""
for (src, dst) in list_message_files(self.distribution.get_name()):
build_dst = os.path.join("build", dst)
self.mkpath(os.path.dirname(build_dst))
self.announce("Compiling %s -> %s" % (src, build_dst))
from linkcheck import msgfmt
msgfmt.make(src, build_dst)
def run (self):
check_manifest()
self.build_message_files()
build.run(self)
class MyClean (clean, object):
"""Custom clean command."""
def run (self):
if self.all:
# remove share directory
directory = os.path.join("build", "share")
if os.path.exists(directory):
remove_tree(directory, dry_run=self.dry_run)
else:
log.warn("'%s' does not exist -- can't clean it", directory)
clean.run(self)
class MySdist (sdist, object):
"""Custom sdist command."""
def get_file_list (self):
"""Add MANIFEST to the file list."""
super(MySdist, self).get_file_list()
self.filelist.append("MANIFEST")
# global include dirs
include_dirs = []
# global macros
define_macros = []
# compiler args
extra_compile_args = []
# library directories
library_dirs = []
# libraries
libraries = []
# scripts
scripts = ['linkchecker', 'linkchecker-gui']
if os.name == 'nt':
# windows does not have unistd.h
define_macros.append(('YY_NO_UNISTD_H', None))
else:
extra_compile_args.append("-pedantic")
if win_compiling:
# we are cross compiling with mingw
# add directory for pyconfig.h
include_dirs.append(win_python_dir)
# add directory for Python.h
include_dirs.append(os.path.join(win_python_dir, "Include"))
# for finding libpythonX.Y.a
library_dirs.append(win_python_dir)
libraries.append("python%s" % get_python_version())
myname = "Bastian Kleineidam"
myemail = "calvin@users.sourceforge.net"
data_files = [
('share/linkchecker',
['config/linkcheckerrc', 'config/logging.conf', ]),
('share/linkchecker/examples',
['cgi-bin/lconline/leer.html.en',
'cgi-bin/lconline/leer.html.de',
'cgi-bin/lconline/index.html',
'cgi-bin/lconline/lc_cgi.html.en',
'cgi-bin/lconline/lc_cgi.html.de',
'cgi-bin/lconline/check.js',
'cgi-bin/lc.cgi',
'cgi-bin/lc.fcgi', ]),
]
if os.name == 'posix':
data_files.append(('share/man/man1', ['doc/en/linkchecker.1', 'doc/en/linkchecker-gui.1']))
data_files.append(('share/man/man5', ['doc/en/linkcheckerrc.5']))
data_files.append(('share/man/de/man1', ['doc/de/linkchecker.1', 'doc/de/linkchecker-gui.1']))
data_files.append(('share/man/de/man5', ['doc/de/linkcheckerrc.5']))
data_files.append(('share/linkchecker/examples',
['config/linkchecker-completion',
'doc/examples/check_blacklist.sh',
'doc/examples/check_for_x_errors.sh',
'doc/examples/check_urls.sh',]))
elif win_compiling:
data_files.append(('share/linkchecker/doc',
['doc/en/documentation.html',
'doc/en/index.html',
'doc/en/install.html',
'doc/en/other.html',
'doc/en/upgrading.html',
'doc/en/lc.css',
'doc/en/navigation.css',
'doc/en/shot1.png',
'doc/en/shot2.png',
'doc/en/shot1_thumb.jpg',
'doc/en/shot2_thumb.jpg',
]))
setup (
name = "linkchecker",
version = "5.0",
description = "check websites and HTML documents for broken links",
keywords = "link,url,checking,verification",
author = myname,
author_email = myemail,
maintainer = myname,
maintainer_email = myemail,
url = "http://linkchecker.sourceforge.net/",
download_url="http://sourceforge.net/project/showfiles.php?group_id=1913",
license = "GPL",
long_description = """Linkchecker features:
o recursive and multithreaded checking
o output in colored or normal text, HTML, SQL, CSV, XML or a sitemap
graph in different formats
o HTTP/1.1, HTTPS, FTP, mailto:, news:, nntp:, Telnet and local file
links support
o restrict link checking with regular expression filters for URLs
o proxy support
o username/password authorization for HTTP, FTP and Telnet
o honors robots.txt exclusion protocol
o Cookie support
o HTML and CSS syntax check
o Antivirus check
o a command line interface
o a (Fast)CGI web interface (requires HTTP server)
""",
distclass = MyDistribution,
cmdclass = {
'install_lib': MyInstallLib,
'install_data': MyInstallData,
'bdist_wininst': MyBdistWininst,
'build_ext': MyBuildExt,
'build': MyBuild,
'clean': MyClean,
'sdist': MySdist,
},
packages = [
'linkcheck', 'linkcheck.logger', 'linkcheck.checker',
'linkcheck.director', 'linkcheck.configuration', 'linkcheck.cache',
'linkcheck.htmlutil', 'linkcheck.dns', 'linkcheck.dns.rdtypes',
'linkcheck.dns.rdtypes.ANY', 'linkcheck.dns.rdtypes.IN',
'linkcheck.HtmlParser', 'linkcheck.network', 'linkcheck.gui',
],
ext_modules = [
Extension('linkcheck.HtmlParser.htmlsax',
sources = [
'linkcheck/HtmlParser/htmllex.c',
'linkcheck/HtmlParser/htmlparse.c',
'linkcheck/HtmlParser/s_util.c',
],
extra_compile_args = extra_compile_args,
library_dirs = library_dirs,
libraries = libraries,
define_macros = define_macros + [('YY_NO_INPUT', None)],
include_dirs = include_dirs + [normpath("linkcheck/HtmlParser")],
),
Extension("linkcheck.network._network",
sources = ["linkcheck/network/_network.c",],
extra_compile_args = extra_compile_args,
library_dirs = library_dirs,
libraries = libraries,
define_macros = define_macros,
include_dirs = include_dirs,
),
],
scripts = scripts,
data_files = data_files,
classifiers = [
'Topic :: Internet :: WWW/HTTP :: Site Management :: Link Checking',
'Development Status :: 5 - Production/Stable',
'License :: OSI Approved :: GNU General Public License (GPL)',
'Programming Language :: Python',
'Programming Language :: C',
],
options = {"py2exe": py2exe_options},
)