Code

Enhance documentation generation.
[roundup.git] / setup.py
1 #! /usr/bin/env python
2 #
3 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
4 # This module is free software, and you may redistribute it and/or modify
5 # under the same terms as Python, so long as this copyright message and
6 # disclaimer are retained in their original form.
7 #
8 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
9 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
10 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
11 # POSSIBILITY OF SUCH DAMAGE.
12 #
13 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
14 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
15 # FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
16 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
17 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
18 #
19 # $Id: setup.py,v 1.105 2008-09-01 01:58:32 richard Exp $
22 from roundup.dist.command.build_doc import build_doc
23 from distutils.core import setup, Extension
24 from distutils.util import get_platform
25 from distutils.file_util import write_file
26 from distutils.command.bdist_rpm import bdist_rpm
27 from distutils.command.build import build
28 from distutils.command.build_scripts import build_scripts
29 from distutils.command.build_py import build_py
31 import sys, os, string
32 from glob import glob
34 # patch distutils if it can't cope with the "classifiers" keyword
35 from distutils.dist import DistributionMetadata
36 if not hasattr(DistributionMetadata, 'classifiers'):
37     DistributionMetadata.classifiers = None
38     DistributionMetadata.download_url = None
40 from roundup import msgfmt
42 def include(d, e):
43     """Generate a pair of (directory, file-list) for installation.
45     'd' -- A directory
47     'e' -- A glob pattern"""
48     
49     return (d, [f for f in glob('%s/%s'%(d, e)) if os.path.isfile(f)])
51 #############################################################################
52 ### Build script files
53 #############################################################################
55 class build_scripts_create(build_scripts):
56     """ Overload the build_scripts command and create the scripts
57         from scratch, depending on the target platform.
59         You have to define the name of your package in an inherited
60         class (due to the delayed instantiation of command classes
61         in distutils, this cannot be passed to __init__).
63         The scripts are created in an uniform scheme: they start the
64         run() function in the module
66             <packagename>.scripts.<mangled_scriptname>
68         The mangling of script names replaces '-' and '/' characters
69         with '-' and '.', so that they are valid module paths.
71         If the target platform is win32, create .bat files instead of
72         *nix shell scripts.  Target platform is set to "win32" if main
73         command is 'bdist_wininst' or if the command is 'bdist' and
74         it has the list of formats (from command line or config file)
75         and the first item on that list is wininst.  Otherwise
76         target platform is set to current (build) platform.
77     """
78     package_name = None
80     def initialize_options(self):
81         build_scripts.initialize_options(self)
82         self.script_preamble = None
83         self.target_platform = None
84         self.python_executable = None
86     def finalize_options(self):
87         build_scripts.finalize_options(self)
88         cmdopt=self.distribution.command_options
90         # find the target platform
91         if self.target_platform:
92             # TODO? allow explicit setting from command line
93             target = self.target_platform
94         if cmdopt.has_key("bdist_wininst"):
95             target = "win32"
96         elif cmdopt.get("bdist", {}).has_key("formats"):
97             formats = cmdopt["bdist"]["formats"][1].split(",")
98             if formats[0] == "wininst":
99                 target = "win32"
100             else:
101                 target = sys.platform
102             if len(formats) > 1:
103                 self.warn(
104                     "Scripts are built for %s only (requested formats: %s)"
105                     % (target, ",".join(formats)))
106         else:
107             # default to current platform
108             target = sys.platform
109         self.target_platfom = target
111         # for native builds, use current python executable path;
112         # for cross-platform builds, use default executable name
113         if self.python_executable:
114             # TODO? allow command-line option
115             pass
116         if target == sys.platform:
117             self.python_executable = os.path.normpath(sys.executable)
118         else:
119             self.python_executable = "python"
121         # for windows builds, add ".bat" extension
122         if target == "win32":
123             # *nix-like scripts may be useful also on win32 (cygwin)
124             # to build both script versions, use:
125             #self.scripts = list(self.scripts) + [script + ".bat"
126             #    for script in self.scripts]
127             self.scripts = [script + ".bat" for script in self.scripts]
129         # tweak python path for installations outside main python library
130         if cmdopt.get("install", {}).has_key("prefix"):
131             prefix = os.path.expanduser(cmdopt['install']['prefix'][1])
132             version = '%d.%d'%sys.version_info[:2]
133             self.script_preamble = '''
134 import sys
135 sys.path.insert(1, "%s/lib/python%s/site-packages")
136 '''%(prefix, version)
137         else:
138             self.script_preamble = ''
140     def copy_scripts(self):
141         """ Create each script listed in 'self.scripts'
142         """
143         if not self.package_name:
144             raise Exception("You have to inherit build_scripts_create and"
145                 " provide a package name")
147         to_module = string.maketrans('-/', '_.')
149         self.mkpath(self.build_dir)
150         for script in self.scripts:
151             outfile = os.path.join(self.build_dir, os.path.basename(script))
153             #if not self.force and not newer(script, outfile):
154             #    self.announce("not copying %s (up-to-date)" % script)
155             #    continue
157             if self.dry_run:
158                 self.announce("would create %s" % outfile)
159                 continue
161             module = os.path.splitext(os.path.basename(script))[0]
162             module = string.translate(module, to_module)
163             script_vars = {
164                 'python': self.python_executable,
165                 'package': self.package_name,
166                 'module': module,
167                 'prefix': self.script_preamble,
168             }
170             self.announce("creating %s" % outfile)
171             file = open(outfile, 'w')
173             try:
174                 # could just check self.target_platform,
175                 # but looking at the script extension
176                 # makes it possible to build both *nix-like
177                 # and windows-like scripts on win32.
178                 # may be useful for cygwin.
179                 if os.path.splitext(outfile)[1] == ".bat":
180                     file.write('@echo off\n'
181                         'if NOT "%%_4ver%%" == "" "%(python)s" -c "from %(package)s.scripts.%(module)s import run; run()" %%$\n'
182                         'if     "%%_4ver%%" == "" "%(python)s" -c "from %(package)s.scripts.%(module)s import run; run()" %%*\n'
183                         % script_vars)
184                 else:
185                     file.write('#! %(python)s\n%(prefix)s'
186                         'from %(package)s.scripts.%(module)s import run\n'
187                         'run()\n'
188                         % script_vars)
189             finally:
190                 file.close()
191                 os.chmod(outfile, 0755)
194 class build_scripts_roundup(build_scripts_create):
195     package_name = 'roundup'
198 def scriptname(path):
199     """ Helper for building a list of script names from a list of
200         module files.
201     """
202     script = os.path.splitext(os.path.basename(path))[0]
203     script = string.replace(script, '_', '-')
204     return script
206 ### Build Roundup
208 def list_message_files(suffix=".po"):
209     """Return list of all found message files and their intallation paths"""
210     _files = glob("locale/*" + suffix)
211     _list = []
212     for _file in _files:
213         # basename (without extension) is a locale name
214         _locale = os.path.splitext(os.path.basename(_file))[0]
215         _list.append((_file, os.path.join(
216             "share", "locale", _locale, "LC_MESSAGES", "roundup.mo")))
217     return _list
219 def check_manifest():
220     """Check that the files listed in the MANIFEST are present when the
221     source is unpacked.
222     """
223     try:
224         f = open('MANIFEST')
225     except:
226         print '\n*** SOURCE WARNING: The MANIFEST file is missing!'
227         return
228     try:
229         manifest = [l.strip() for l in f.readlines()]
230     finally:
231         f.close()
232     err = [line for line in manifest if not os.path.exists(line)]
233     err.sort()
234     # ignore auto-generated files
235     if err == ['roundup-admin', 'roundup-demo', 'roundup-gettext',
236             'roundup-mailgw', 'roundup-server']:
237         err = []
238     if err:
239         n = len(manifest)
240         print '\n*** SOURCE WARNING: There are files missing (%d/%d found)!'%(
241             n-len(err), n)
242         print 'Missing:', '\nMissing: '.join(err)
245 class build_py_roundup(build_py):
247     def find_modules(self):
248         # Files listed in py_modules are in the toplevel directory
249         # of the source distribution.
250         modules = []
251         for module in self.py_modules:
252             path = string.split(module, '.')
253             package = string.join(path[0:-1], '.')
254             module_base = path[-1]
255             module_file = module_base + '.py'
256             if self.check_module(module, module_file):
257                 modules.append((package, module_base, module_file))
258         return modules
261 class build_roundup(build):
263     def build_message_files(self):
264         """For each locale/*.po, build .mo file in target locale directory"""
265         for (_src, _dst) in list_message_files():
266             _build_dst = os.path.join("build", _dst)
267             self.mkpath(os.path.dirname(_build_dst))
268             self.announce("Compiling %s -> %s" % (_src, _build_dst))
269             msgfmt.make(_src, _build_dst)
271     def run(self):
272         check_manifest()
273         self.build_message_files()
274         build.run(self)
276 class bdist_rpm_roundup(bdist_rpm):
278     def finalize_options(self):
279         bdist_rpm.finalize_options(self)
280         if self.install_script:
281             # install script is overridden.  skip default
282             return
283         # install script option must be file name.
284         # create the file in rpm build directory.
285         install_script = os.path.join(self.rpm_base, "install.sh")
286         self.mkpath(self.rpm_base)
287         self.execute(write_file, (install_script, [
288                 ("%s setup.py install --root=$RPM_BUILD_ROOT "
289                     "--record=ROUNDUP_FILES") % self.python,
290                 # allow any additional extension for man pages
291                 # (rpm may compress them to .gz or .bz2)
292                 # man page here is any file
293                 # with single-character extension
294                 # in man directory
295                 "sed -e 's,\(/man/.*\..\)$,\\1*,' "
296                     "<ROUNDUP_FILES >INSTALLED_FILES",
297             ]), "writing '%s'" % install_script)
298         self.install_script = install_script
300 #############################################################################
301 ### Main setup stuff
302 #############################################################################
304 def main():
305     # build list of scripts from their implementation modules
306     roundup_scripts = map(scriptname, glob('roundup/scripts/[!_]*.py'))
308     # template munching
309     packagelist = [
310         'roundup',
311         'roundup.cgi',
312         'roundup.cgi.PageTemplates',
313         'roundup.cgi.TAL',
314         'roundup.cgi.ZTUtils',
315         'roundup.backends',
316         'roundup.scripts',
317     ]
318     installdatafiles = [
319         ('share/roundup/cgi-bin', ['frontends/roundup.cgi']),
320     ]
321     py_modules = ['roundup.demo',]
323     # install man pages on POSIX platforms
324     if os.name == 'posix':
325         installdatafiles.append(('man/man1', ['doc/roundup-admin.1',
326             'doc/roundup-mailgw.1', 'doc/roundup-server.1',
327             'doc/roundup-demo.1']))
329     # add the templates to the data files lists
330     from roundup.init import listTemplates
331     templates = [t['path'] for t in listTemplates('templates').values()]
332     for tdir in templates:
333         # scan for data files
334         for idir in '. detectors extensions html'.split():
335             idir = os.path.join(tdir, idir)
336             if not os.path.isdir(idir):
337                 continue
338             tfiles = []
339             for f in os.listdir(idir):
340                 if f.startswith('.'):
341                     continue
342                 ifile = os.path.join(idir, f)
343                 if os.path.isfile(ifile):
344                     tfiles.append(ifile)
345             installdatafiles.append(
346                 (os.path.join('share', 'roundup', idir), tfiles)
347             )
349     # add message files
350     for (_dist_file, _mo_file) in list_message_files():
351         installdatafiles.append((os.path.dirname(_mo_file),
352             [os.path.join("build", _mo_file)]))
354     # add docs
355     installdatafiles.append(include(os.path.join('share', 'doc', 'roundup', 'html'), '*'))
357     # perform the setup action
358     from roundup import __version__
359     setup_args = {
360         'name': "roundup",
361         'version': __version__,
362         'description': "A simple-to-use and -install issue-tracking system"
363             " with command-line, web and e-mail interfaces. Highly"
364             " customisable.",
365         'long_description':
366 '''In this release
367 ===============
369 1.4.6 is a bugfix release:
371 - Fix bug introduced in 1.4.5 in RDBMS full-text indexing
372 - Make URL matching code less matchy
374 If you're upgrading from an older version of Roundup you *must* follow
375 the "Software Upgrade" guidelines given in the maintenance documentation.
377 Roundup requires python 2.3 or later for correct operation.
379 To give Roundup a try, just download (see below), unpack and run::
381     roundup-demo
383 Documentation is available at the website:
384      http://roundup.sourceforge.net/
385 Mailing lists - the place to ask questions:
386      http://sourceforge.net/mail/?group_id=31577
388 About Roundup
389 =============
391 Roundup is a simple-to-use and -install issue-tracking system with
392 command-line, web and e-mail interfaces. It is based on the winning design
393 from Ka-Ping Yee in the Software Carpentry "Track" design competition.
395 Note: Ping is not responsible for this project. The contact for this
396 project is richard@users.sourceforge.net.
398 Roundup manages a number of issues (with flexible properties such as
399 "description", "priority", and so on) and provides the ability to:
401 (a) submit new issues,
402 (b) find and edit existing issues, and
403 (c) discuss issues with other participants.
405 The system will facilitate communication among the participants by managing
406 discussions and notifying interested parties when issues are edited. One of
407 the major design goals for Roundup that it be simple to get going. Roundup
408 is therefore usable "out of the box" with any python 2.3+ installation. It
409 doesn't even need to be "installed" to be operational, though a
410 disutils-based install script is provided.
412 It comes with two issue tracker templates (a classic bug/feature tracker and
413 a minimal skeleton) and five database back-ends (anydbm, sqlite, metakit,
414 mysql and postgresql).
415 ''',
416         'author': "Richard Jones",
417         'author_email': "richard@users.sourceforge.net",
418         'url': 'http://roundup.sourceforge.net/',
419         'packages': packagelist,
420         'classifiers': [
421             'Development Status :: 5 - Production/Stable',
422             'Environment :: Console',
423             'Environment :: Web Environment',
424             'Intended Audience :: End Users/Desktop',
425             'Intended Audience :: Developers',
426             'Intended Audience :: System Administrators',
427             'License :: OSI Approved :: Python Software Foundation License',
428             'Operating System :: MacOS :: MacOS X',
429             'Operating System :: Microsoft :: Windows',
430             'Operating System :: POSIX',
431             'Programming Language :: Python',
432             'Topic :: Communications :: Email',
433             'Topic :: Office/Business',
434             'Topic :: Software Development :: Bug Tracking',
435         ],
437         # Override certain command classes with our own ones
438         'cmdclass': {
439         'build_doc': build_doc,
440             'build_scripts': build_scripts_roundup,
441             'build_py': build_py_roundup,
442             'build': build_roundup,
443             'bdist_rpm': bdist_rpm_roundup,
444         },
445         'scripts': roundup_scripts,
447         'data_files':  installdatafiles
448     }
449     if sys.version_info[:2] > (2, 2):
450        setup_args['py_modules'] = py_modules
452     setup(**setup_args)
454 if __name__ == '__main__':
455     main()
457 # vim: set filetype=python sts=4 sw=4 et si :