3
This provides a setup() method for distutils and DistUtilsExtra which infers as
4
many setup() arguments as possible. The idea is that your setup.py only needs
5
to have the metadata and some tweaks for unusual files/paths, in a "convention
6
over configuration" paradigm.
8
This currently supports:
10
* Python modules (./*.py, only in root directory)
11
* Python packages (all directories with __init__.py)
13
* Qt4 user interfaces (*.ui)
14
* D-Bus (*.conf and *.service)
15
* PolicyKit (*.policy.in)
16
* Desktop files (*.desktop.in)
17
* KDE4 notifications (*.notifyrc.in)
18
* scripts (all in bin/, and ./<projectname>
19
* Auxiliary data files (in data/*)
20
* automatic po/POTFILES.in (with all source files which contain _())
21
* automatic MANIFEST (everything except swap and backup files, *.pyc, and
24
* files which should go into /etc (./etc/*, copied verbatim)
25
* determining "requires" from import statements in source code
26
* determining "provides" from shipped packages and modules
28
If you follow above conventions, then you don't need any po/POTFILES.in,
29
./setup.cfg, or ./MANIFEST.in, and just need the project metadata (name,
30
author, license, etc.) in ./setup.py.
35
# (c) 2009 Canonical Ltd.
36
# Author: Martin Pitt <martin.pitt@ubuntu.com>
38
import os, os.path, fnmatch, stat, sys
39
import compiler # TODO: deprecated
42
from DistUtilsExtra.command import *
43
import distutils.dir_util
44
import distutils.command.clean
45
import distutils.command.sdist
46
import distutils.command.install
47
import distutils.filelist
49
# FIXME: global variable, to share with build_i18n_auto
54
'''Auto-inferring extension of standard distutils.core.setup()'''
57
src_all = src_find(attrs)
60
# src_find() removes explicit scripts, but we need them for automatic
61
# POTFILE.in building and requires
62
src_all.update(set(attrs.get('scripts', [])))
64
src_mark(src, 'setup.py')
66
# mark files in etc/*, handled by install_auto
68
if f.startswith('etc' + os.path.sep):
73
__packages(attrs, src)
74
__provides(attrs, src)
78
__stdfiles(attrs, src)
79
__gtkbuilder(attrs, src)
80
__manpages(attrs, src)
82
if 'clean' not in sys.argv:
83
__requires(attrs, src_all)
85
distutils.core.setup(**attrs)
88
print 'WARNING: the following files are not recognized by DistUtilsExtra.auto:'
96
class clean_build_tree(distutils.command.clean.clean):
98
description = 'clean up build/ directory'
102
if os.path.isdir('build'):
103
distutils.dir_util.remove_tree('build')
104
distutils.command.clean.clean.run(self)
106
def __cmdclass(attrs):
107
'''Default cmdclass for DistUtilsExtra'''
109
v = attrs.setdefault('cmdclass', {})
110
v.setdefault('build', build_extra.build_extra)
111
v.setdefault('build_i18n', build_i18n_auto)
112
v.setdefault('build_icons', build_icons.build_icons)
113
v.setdefault('build_kdeui', build_kdeui_auto)
114
v.setdefault('install', install_auto)
115
v.setdefault('clean', clean_build_tree)
116
v.setdefault('sdist', sdist_auto)
118
def __modules(attrs, src):
119
'''Default modules'''
121
if 'py_modules' in attrs:
122
for mod in attrs['py_modules']:
123
src_markglob(src, os.path.join(mod, '*.py'))
126
mods = attrs.setdefault('py_modules', [])
128
for f in src_fileglob(src, '*.py'):
129
if os.path.sep not in f:
130
mods.append(os.path.splitext(f)[0])
133
def __packages(attrs, src):
134
'''Default packages'''
136
if 'packages' in attrs:
137
for pkg in attrs['packages']:
138
src_markglob(src, os.path.join(pkg, '*.py'))
141
packages = attrs.setdefault('packages', [])
143
for f in src_fileglob(src, '__init__.py'):
144
pkg = os.path.dirname(f)
146
src_markglob(src, os.path.join(pkg, '*.py'))
148
def __dbus(attrs, src):
149
'''D-Bus configuration and services'''
151
v = attrs.setdefault('data_files', [])
153
# /etc/dbus-1/system.d/*.conf
155
for f in src_fileglob(src, '*.conf'):
156
if '-//freedesktop//DTD D-BUS Bus Configuration' in open(f).read():
160
v.append(('/etc/dbus-1/system.d/', dbus_conf))
165
for f in src_fileglob(src, '*.service'):
166
lines = [l.strip() for l in open(f).readlines()]
167
if '[D-BUS Service]' not in lines:
170
if l.startswith('User='):
172
system_service.append(f)
176
session_service.append(f)
178
v.append(('share/dbus-1/system-services', system_service))
180
v.append(('share/dbus-1/services', session_service))
182
def __data(attrs, src):
183
'''Install auxiliary data files.
185
This installs everything from data/ except data/icons/ into
186
prefix/share/<projectname>/.
188
v = attrs.setdefault('data_files', [])
190
assert 'name' in attrs, 'You need to set the "name" property in setup.py'
194
if f.startswith('data/') and not f.startswith('data/icons/'):
195
v.append((os.path.join('share', attrs['name'], os.path.dirname(f[5:])), [f]))
198
def __scripts(attrs, src):
201
This picks executable scripts in bin/*, and an executable ./<projectname>.
202
Other scripts have to be added manually; this is to avoid automatically
203
installing test suites, build scripts, etc.
205
assert 'name' in attrs, 'You need to set the "name" property in setup.py'
209
if f.startswith('bin/') or f == attrs['name']:
211
if stat.S_ISREG(st.st_mode) and st.st_mode & stat.S_IEXEC:
216
v = attrs.setdefault('scripts', [])
219
def __stdfiles(attrs, src):
220
'''Install/mark standard files.
222
This covers COPYING, AUTHORS, README, etc.
224
src_markglob(src, 'COPYING*')
225
src_markglob(src, 'LICENSE*')
226
src_markglob(src, 'AUTHORS')
227
src_markglob(src, 'MANIFEST.in')
228
src_markglob(src, 'MANIFEST')
229
src_markglob(src, 'TODO')
231
# install all README* from the root directory
233
for f in src_fileglob(src, 'README*').union(src_fileglob(src, 'NEWS')):
234
if os.path.sep not in f:
238
assert 'name' in attrs, 'You need to set the "name" property in setup.py'
240
attrs.setdefault('data_files', []).append((os.path.join('share', 'doc',
241
attrs['name']), readme))
243
def __gtkbuilder(attrs, src):
244
'''Install GtkBuilder *.ui files'''
247
for f in src_fileglob(src_all, '*.ui'):
248
contents = open(f).read()
249
if '<interface>\n' in contents and 'class="Gtk' in contents:
253
assert 'name' in attrs, 'You need to set the "name" property in setup.py'
255
attrs.setdefault('data_files', []).append((os.path.join('share',
258
def __manpages(attrs, src):
259
'''Install manpages'''
262
for f in src_fileglob(src_all, '*.[0123456789]'):
263
line = open(f).readline()
264
if line.startswith('.TH '):
266
mans.setdefault(f[-1], []).append(f)
267
v = attrs.setdefault('data_files', [])
268
for section, files in mans.iteritems():
269
v.append((os.path.join('share', 'man', 'man' + section), files))
271
def __external_mod(module, attrs):
272
'''Check if given Python module is not included in Python or locally'''
274
# filter out locally provided modules
275
if module in attrs['provides']:
277
for m in _module_parents(module):
278
if m in attrs['provides']:
282
path = __import__(module).__file__
284
print >> sys.stderr, 'ERROR: Python module %s not found' % module
286
except AttributeError: # builtin modules
289
return 'dist-packages' in path or 'site-packages' in path or \
290
not path.startswith(os.path.dirname(os.__file__))
292
def __add_imports(imports, file, attrs):
293
'''Add all imported modules from file to imports set.
295
This filters out modules which are shipped with Python itself.
298
ast = compiler.parseFile(file)
300
for node in ast.node.nodes:
301
if isinstance(node, compiler.ast.Import):
302
for name, _ in node.names:
303
if __external_mod(name, attrs):
305
if isinstance(node, compiler.ast.From):
306
if __external_mod(node.modname, attrs):
307
imports.add(node.modname)
308
except SyntaxError, e:
309
print >> sys.stderr, 'WARNING: syntax errors in', f, ':', e
311
def _module_parents(mod):
312
'''Iterate over all parents of a module'''
314
hierarchy = mod.split('.')
317
yield '.'.join(hierarchy)
320
def __filter_namespace(modules):
321
'''Filter out modules which are already covered by a parent module
323
E. g. this transforms ['os.path', 'os', 'foo.bar.baz', 'foo.bar'] to
329
for p in _module_parents(m):
335
return sorted(result)
337
def __requires(attrs, src_all):
338
'''Determine requires (if not set explicitly)'''
340
if 'requires' in attrs:
345
# iterate over all *.py and scripts which are Python
347
ext = os.path.splitext(s)[1]
351
if not line.startswith('#!') or 'python' not in line:
355
__add_imports(imports, s, attrs)
357
attrs['requires'] = __filter_namespace(imports)
359
def __provides(attrs, src_all):
360
'''Determine provides (if not set explicitly)'''
362
if 'provides' in attrs:
365
provides = attrs.get('py_modules', [])
366
for p in attrs.get('packages', []):
367
provides.append(p.replace(os.path.sep, '.'))
368
attrs['provides'] = __filter_namespace(provides)
375
'''Find source files.
377
This ignores all source files which are explicitly specified as setup()
382
# files explicitly covered in setup() call
383
explicit = set(attrs.get('scripts', []))
384
for (destdir, files) in attrs.get('data_files', []):
385
explicit.update(files)
387
for (root, dirs, files) in os.walk('.'):
388
if root.startswith('./'):
392
if root.startswith('.') or \
393
root.split(os.path.sep, 1)[0] in ('build', 'test', 'tests'):
395
# data/icons is handled by build_icons
396
if root.startswith(os.path.join('data', 'icons')):
399
ext = os.path.splitext(f)[1]
400
if f.startswith('.') or ext in ('.pyc', '~', '.mo'):
402
# po/*.po is taken care of by build_i18n
403
if root == 'po' and (ext == '.po' or f == 'POTFILES.in'):
406
path = os.path.join(root, f)
407
if path not in explicit:
412
def src_fileglob(src, fnameglob):
413
'''Return set of files which match fnameglob.'''
417
if fnmatch.fnmatch(os.path.basename(f), fnameglob):
421
def src_mark(src, path):
422
'''Remove path from src.'''
426
def src_markglob(src, pathglob):
427
'''Remove all paths from src which match pathglob.'''
430
if fnmatch.fnmatch(f, pathglob):
434
# Automatic setup.cfg
437
class build_i18n_auto(build_i18n.build_i18n):
438
def finalize_options(self):
439
build_i18n.build_i18n.finalize_options(self)
442
# add PolicyKit files
444
for f in src_fileglob(src, '*.policy.in'):
446
policy_files.append(f)
449
xf = eval(self.xml_files)
452
xf.append(('share/PolicyKit/policy', policy_files))
453
self.xml_files = repr(xf)
459
for f in src_fileglob(src, '*.desktop.in'):
462
autostart_files.append(f)
464
desktop_files.append(f)
465
for f in src_fileglob(src, '*.notifyrc.in'):
467
notify_files.append(f)
469
df = eval(self.desktop_files)
473
df.append(('share/applications', desktop_files))
475
df.append(('share/autostart', autostart_files))
477
df.append(('share/kde4/apps/' + self.distribution.get_name(), notify_files))
478
self.desktop_files = repr(df)
480
# mark PO template as known to handle
482
src_mark(src, os.path.join('po', self.distribution.get_name() + '.pot'))
487
'''Build a default POTFILES.in'''
489
auto_potfiles_in = False
492
if not os.path.exists(os.path.join('po', 'POTFILES.in')):
493
files = src_fileglob(src_all, '*.py')
494
files.update(src_fileglob(src_all, '*.desktop.in'))
495
files.update(src_fileglob(src_all, '*.notifyrc.in'))
496
files.update(src_fileglob(src_all, '*.policy.in'))
498
for f in src_fileglob(src_all, '*.ui'):
499
contents = open(f).read()
500
if '<interface>\n' in contents and '<requires lib="gtk+"' in contents:
501
files.add('[type: gettext/glade]' + f)
503
# find extensionless executable scripts which are Python files, and
504
# generate a temporary *.py alias, so that they get caught by
508
if os.access(f, os.X_OK) and os.path.splitext(f)[1] == '' and \
509
not os.path.exists(f_py):
510
line = open(f).readline()
511
if line.startswith('#!') and 'python' in line:
512
os.symlink(os.path.basename(f), f_py)
514
exe_symlinks.append(f_py)
517
if not os.path.isdir('po'):
519
potfiles_in = open('po/POTFILES.in', 'w')
520
print >> potfiles_in, '[encoding: UTF-8]'
522
print >> potfiles_in, f
525
auto_potfiles_in = True
527
build_i18n.build_i18n.run(self)
529
for f in exe_symlinks:
533
os.unlink('po/POTFILES.in')
539
class build_kdeui_auto(build_kdeui.build_kdeui):
540
def finalize_options(self):
543
# add *.ui files which belong to KDE4
545
for f in src_fileglob(src, '*.ui'):
546
if open(f).readline().startswith('<ui version="'):
548
kdeui_files.append(f)
551
uf = eval(self.ui_files)
555
self.ui_files = repr(uf)
557
build_kdeui.build_kdeui.finalize_options(self)
563
class sdist_auto(distutils.command.sdist.sdist):
564
def add_defaults(self):
565
filter_prefix = ['build', '.git', '.svn', '.CVS', '.bzr',
566
os.path.join('dist', self.distribution.get_name())]
567
filter_suffix = ['.pyc', '.mo', '~', '.swp']
569
distutils.command.sdist.sdist.add_defaults(self)
571
manifest_in = os.path.join('MANIFEST.in')
572
if os.path.exists(manifest_in):
575
for f in distutils.filelist.findall():
576
if f in self.filelist.files or any(map(f.startswith, filter_prefix)) or \
577
any(map(f.endswith, filter_suffix)):
579
self.filelist.append(f)
581
# Automatic installation of ./etc/
583
class install_auto(distutils.command.install.install):
585
# install files from etc/
586
if os.path.isdir('etc'):
587
# work around a bug in copy_tree() which fails with "File exists" on
588
# previously existing symlinks
589
for f in distutils.filelist.findall():
590
if not f.startswith('etc' + os.path.sep) or not os.path.islink(f):
593
os.unlink(os.path.join(self.root, f))
597
distutils.dir_util.copy_tree('etc', os.path.join(self.root, 'etc'),
598
preserve_times=0, preserve_symlinks=1, verbose=1)
600
distutils.command.install.install.run(self)