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)
12
* Docbook-XML GNOME help files (help/<language>/{*.xml,*.omf,figures})
13
* GtkBuilder (*.ui) [installed into prefix/share/<projectname>/]
14
* Qt4 user interfaces (*.ui) [compiled with pykdeuic into Python modules]
15
* D-Bus (*.conf and *.service)
16
* PolicyKit (*.policy.in)
17
* Desktop files (*.desktop.in) [into prefix/share/applications, or
18
prefix/share/autostart if they have "autostart" anywhere in the path]
19
* KDE4 notifications (*.notifyrc.in)
20
* Apport hooks (apport/*) [installed into /usr/share/apport/package-hooks]
21
* scripts (all in bin/, and ./<projectname>
22
* Auxiliary data files (in data/*) [into prefix/share/<projectname>/]
23
* automatic po/POTFILES.in (with all source files which contain _())
24
* automatic MANIFEST (everything except swap and backup files, *.pyc, and
27
* icons (data/icons/<size>/<category>/*.{svg,png})
28
* files which should go into /etc (./etc/*, copied verbatim)
29
* determining "requires" from import statements in source code
30
* determining "provides" from shipped packages and modules
32
If you follow above conventions, then you don't need any po/POTFILES.in,
33
./setup.cfg, or ./MANIFEST.in, and just need the project metadata (name,
34
author, license, etc.) in ./setup.py.
37
# (c) 2009 Canonical Ltd.
38
# Author: Martin Pitt <martin.pitt@ubuntu.com>
40
import os, os.path, fnmatch, stat, sys, subprocess
44
from DistUtilsExtra import __version__ as __pkgversion
45
from DistUtilsExtra.command import *
46
import distutils.dir_util
47
import distutils.command.clean
48
import distutils.command.sdist
49
import distutils.command.install
50
import distutils.filelist
52
__version__ = __pkgversion
54
# FIXME: global variable, to share with build_i18n_auto
59
'''Auto-inferring extension of standard distutils.core.setup()'''
62
src_all = src_find(attrs)
65
# src_find() removes explicit scripts, but we need them for automatic
66
# POTFILE.in building and requires
67
src_all.update(set(attrs.get('scripts', [])))
69
src_mark(src, 'setup.py')
70
src_markglob(src, 'setup.cfg')
72
# mark files in etc/*, handled by install_auto
73
# don't install DistUtilsExtra if bundled with a source tarball
75
ignore_dirs = ['etc', 'DistUtilsExtra', 'debian']
79
if f.startswith(d + os.path.sep):
84
__packages(attrs, src)
85
__provides(attrs, src)
87
__apport_hooks(attrs, src)
90
__stdfiles(attrs, src)
91
__gtkbuilder(attrs, src)
92
__manpages(attrs, src)
94
if 'clean' not in sys.argv:
95
__requires(attrs, src_all)
97
distutils.core.setup(**attrs)
100
print 'WARNING: the following files are not recognized by DistUtilsExtra.auto:'
101
for f in sorted(src):
108
class clean_build_tree(distutils.command.clean.clean):
110
description = 'clean up build/ directory'
114
if os.path.isdir('build'):
115
distutils.dir_util.remove_tree('build')
116
distutils.command.clean.clean.run(self)
118
def __cmdclass(attrs):
119
'''Default cmdclass for DistUtilsExtra'''
121
v = attrs.setdefault('cmdclass', {})
122
v.setdefault('build', build_extra.build_extra)
123
v.setdefault('build_help', build_help_auto)
124
v.setdefault('build_i18n', build_i18n_auto)
125
v.setdefault('build_icons', build_icons.build_icons)
126
v.setdefault('build_kdeui', build_kdeui_auto)
127
v.setdefault('install', install_auto)
128
v.setdefault('clean', clean_build_tree)
129
v.setdefault('sdist', sdist_auto)
131
def __modules(attrs, src):
132
'''Default modules'''
134
if 'py_modules' in attrs:
135
for mod in attrs['py_modules']:
136
src_markglob(src, os.path.join(mod, '*.py'))
139
mods = attrs.setdefault('py_modules', [])
141
for f in src_fileglob(src, '*.py'):
142
if os.path.sep not in f:
143
mods.append(os.path.splitext(f)[0])
146
def __packages(attrs, src):
147
'''Default packages'''
149
if 'packages' in attrs:
150
for pkg in attrs['packages']:
151
src_markglob(src, os.path.join(pkg, '*.py'))
154
packages = attrs.setdefault('packages', [])
156
for f in src_fileglob(src, '__init__.py'):
157
if f.startswith('data' + os.path.sep):
159
pkg = os.path.dirname(f)
161
src_markglob(src, os.path.join(pkg, '*.py'))
163
def __dbus(attrs, src):
164
'''D-Bus configuration and services'''
166
v = attrs.setdefault('data_files', [])
168
# /etc/dbus-1/system.d/*.conf
170
for f in src_fileglob(src, '*.conf'):
171
if '-//freedesktop//DTD D-BUS Bus Configuration' in open(f).read():
175
v.append(('/etc/dbus-1/system.d/', dbus_conf))
180
for f in src_fileglob(src, '*.service'):
181
lines = [l.strip() for l in open(f).readlines()]
182
if '[D-BUS Service]' not in lines:
185
if l.startswith('User='):
187
system_service.append(f)
191
session_service.append(f)
193
v.append(('share/dbus-1/system-services', system_service))
195
v.append(('share/dbus-1/services', session_service))
197
def __apport_hooks(attrs, src):
199
v = attrs.setdefault('data_files', [])
201
# files will be copied to /usr/share/apport/package-hooks/
203
assert 'name' in attrs, 'You need to set the "name" property in setup.py'
204
for f in src_fileglob(src, '*.py'):
205
if f.startswith('apport/'):
209
v.append(('share/apport/package-hooks/', hooks))
211
def __data(attrs, src):
212
'''Install auxiliary data files.
214
This installs everything from data/ except data/icons/ and *.in files (which
215
are handled differently) into prefix/share/<projectname>/.
217
v = attrs.setdefault('data_files', [])
219
assert 'name' in attrs, 'You need to set the "name" property in setup.py'
223
if f.startswith('data/') and not f.startswith('data/icons/') and \
224
not f.endswith('.desktop.in') and not f.endswith('*.notifyrc.in'):
225
if not os.path.islink(f):
226
# symlinks are handled in install_auto
227
v.append((os.path.join('share', attrs['name'], os.path.dirname(f[5:])), [f]))
230
def __scripts(attrs, src):
233
This picks executable scripts in bin/*, and an executable ./<projectname>.
234
Other scripts have to be added manually; this is to avoid automatically
235
installing test suites, build scripts, etc.
237
assert 'name' in attrs, 'You need to set the "name" property in setup.py'
241
if f.startswith('bin/') or f == attrs['name']:
243
if stat.S_ISREG(st.st_mode) and st.st_mode & stat.S_IEXEC:
246
elif stat.S_ISLNK(st.st_mode):
247
# symlinks are handled in install_auto
251
v = attrs.setdefault('scripts', [])
254
def __stdfiles(attrs, src):
255
'''Install/mark standard files.
257
This covers COPYING, AUTHORS, README, etc.
259
src_markglob(src, 'COPYING*')
260
src_markglob(src, 'LICENSE*')
261
src_markglob(src, 'AUTHORS')
262
src_markglob(src, 'MANIFEST.in')
263
src_markglob(src, 'MANIFEST')
264
src_markglob(src, 'TODO')
266
# install all README* from the root directory
268
for f in src_fileglob(src, 'README*').union(src_fileglob(src, 'NEWS')):
269
if os.path.sep not in f:
273
assert 'name' in attrs, 'You need to set the "name" property in setup.py'
275
attrs.setdefault('data_files', []).append((os.path.join('share', 'doc',
276
attrs['name']), readme))
278
def __gtkbuilder(attrs, src):
279
'''Install GtkBuilder *.ui files'''
282
for f in src_fileglob(src, '*.ui'):
283
contents = open(f).read()
284
if ('<interface>\n' in contents or '<interface ' in contents) and 'class="Gtk' in contents:
288
assert 'name' in attrs, 'You need to set the "name" property in setup.py'
290
attrs.setdefault('data_files', []).append((os.path.join('share',
293
def __manpages(attrs, src):
294
'''Install manpages'''
297
for f in src_fileglob(src, '*.[0123456789]'):
298
line = open(f).readline()
299
if line.startswith('.TH '):
301
mans.setdefault(f[-1], []).append(f)
302
v = attrs.setdefault('data_files', [])
303
for section, files in mans.iteritems():
304
v.append((os.path.join('share', 'man', 'man' + section), files))
306
def __external_mod(module, attrs):
307
'''Check if given Python module is not included in Python or locally'''
309
# filter out locally provided modules
310
if module in attrs['provides']:
312
for m in _module_parents(module):
313
if m in attrs['provides']:
317
path = __import__(module).__file__
319
print >> sys.stderr, 'ERROR: Python module %s not found' % module
321
except AttributeError: # builtin modules
324
return 'dist-packages' in path or 'site-packages' in path or \
325
not path.startswith(os.path.dirname(os.__file__))
327
def __add_imports(imports, file, attrs):
328
'''Add all imported modules from file to imports set.
330
This filters out modules which are shipped with Python itself.
333
tree = ast.parse(open(file).read(), file)
335
for node in ast.walk(tree):
336
if isinstance(node, ast.Import):
337
for alias in node.names:
338
if __external_mod(alias.name, attrs):
339
imports.add(alias.name)
340
if isinstance(node, ast.ImportFrom):
341
if __external_mod(node.module, attrs):
342
imports.add(node.module)
343
except SyntaxError, e:
344
print >> sys.stderr, 'WARNING: syntax errors in', file, ':', e
346
def _module_parents(mod):
347
'''Iterate over all parents of a module'''
349
hierarchy = mod.split('.')
352
yield '.'.join(hierarchy)
355
def __filter_namespace(modules):
356
'''Filter out modules which are already covered by a parent module
358
E. g. this transforms ['os.path', 'os', 'foo.bar.baz', 'foo.bar'] to
364
for p in _module_parents(m):
370
return sorted(result)
372
def __requires(attrs, src_all):
373
'''Determine requires (if not set explicitly)'''
375
if 'requires' in attrs:
380
# iterate over all *.py and scripts which are Python
382
if s.startswith('data' + os.path.sep):
384
ext = os.path.splitext(s)[1]
388
if not line.startswith('#!') or 'python' not in line:
392
__add_imports(imports, s, attrs)
394
attrs['requires'] = __filter_namespace(imports)
396
def __provides(attrs, src_all):
397
'''Determine provides (if not set explicitly)'''
399
if 'provides' in attrs:
402
provides = attrs.get('py_modules', [])
403
for p in attrs.get('packages', []):
404
provides.append(p.replace(os.path.sep, '.'))
405
attrs['provides'] = __filter_namespace(provides)
412
'''Find source files.
414
This ignores all source files which are explicitly specified as setup()
419
# files explicitly covered in setup() call
420
explicit = set(attrs.get('scripts', []))
421
for (destdir, files) in attrs.get('data_files', []):
422
explicit.update(files)
424
for (root, dirs, files) in os.walk('.'):
425
if root.startswith('./'):
429
if root.startswith('.') or \
430
root.split(os.path.sep, 1)[0] in ('build', 'test', 'tests'):
432
# data/icons is handled by build_icons
433
if root.startswith(os.path.join('data', 'icons')):
436
ext = os.path.splitext(f)[1]
437
if f.startswith('.') or ext in ('.pyc', '~', '.mo'):
439
# po/*.po is taken care of by build_i18n
440
if root == 'po' and (ext == '.po' or f == 'POTFILES.in'):
443
path = os.path.join(root, f)
444
if path not in explicit:
449
def src_fileglob(src, fnameglob):
450
'''Return set of files which match fnameglob.'''
454
if fnmatch.fnmatch(os.path.basename(f), fnameglob):
458
def src_mark(src, path):
459
'''Remove path from src.'''
463
def src_markglob(src, pathglob):
464
'''Remove all paths from src which match pathglob.'''
467
if fnmatch.fnmatch(f, pathglob):
471
# Automatic setup.cfg
474
class build_help_auto(build_help.build_help):
475
def finalize_options(self):
476
build_help.build_help.finalize_options(self)
479
for data_set in self.get_data_files():
480
for filepath in data_set[1]:
483
class build_i18n_auto(build_i18n.build_i18n):
484
def finalize_options(self):
485
build_i18n.build_i18n.finalize_options(self)
489
# add PolicyKit files
491
for f in src_fileglob(src, '*.policy.in'):
493
policy_files.append(f)
495
# check if we have PolicyKit 1 API
496
if subprocess.call(['grep', '-q', 'org\.freedesktop\.PolicyKit1'] +
497
list(src_fileglob(src_all, '*.py')),
498
stderr=subprocess.PIPE) == 0:
499
destdir = os.path.join('share', 'polkit-1', 'actions')
501
destdir = os.path.join('share', 'PolicyKit', 'policy')
503
xf = eval(self.xml_files)
506
xf.append((destdir, policy_files))
507
self.xml_files = repr(xf)
513
for f in src_fileglob(src, '*.desktop.in'):
516
autostart_files.append(f)
518
desktop_files.append(f)
519
for f in src_fileglob(src, '*.notifyrc.in'):
521
notify_files.append(f)
523
df = eval(self.desktop_files)
527
df.append(('share/applications', desktop_files))
529
df.append(('share/autostart', autostart_files))
531
df.append(('share/kde4/apps/' + self.distribution.get_name(), notify_files))
532
self.desktop_files = repr(df)
534
# mark PO template as known to handle
536
src_mark(src, os.path.join(self.po_dir, self.distribution.get_name() + '.pot'))
541
'''Build a default POTFILES.in'''
543
auto_potfiles_in = False
547
if not os.path.exists(os.path.join('po', 'POTFILES.in')):
548
files = src_fileglob(src_all, '*.py')
549
files.update(src_fileglob(src_all, '*.desktop.in'))
550
files.update(src_fileglob(src_all, '*.notifyrc.in'))
551
files.update(src_fileglob(src_all, '*.policy.in'))
553
for f in src_fileglob(src_all, '*.ui'):
554
contents = open(f).read()
555
if ('<interface>\n' in contents or '<interface ' in contents) and 'class="Gtk' in contents:
556
files.add('[type: gettext/glade]' + f)
558
# find extensionless executable scripts which are Python files, and
559
# generate a temporary *.py alias, so that they get caught by
561
for f in reduce(lambda x, y: x.union(y[1]), self.distribution.data_files, src_all):
563
if os.access(f, os.X_OK) and os.path.splitext(f)[1] == '' and \
564
not os.path.exists(f_py):
565
line = open(f).readline()
566
if line.startswith('#!') and 'python' in line:
567
os.symlink(os.path.basename(f), f_py)
569
exe_symlinks.append(f_py)
572
if not os.path.isdir('po'):
574
potfiles_in = open('po/POTFILES.in', 'w')
575
print >> potfiles_in, '[encoding: UTF-8]'
577
print >> potfiles_in, f
580
auto_potfiles_in = True
582
build_i18n.build_i18n.run(self)
584
for f in exe_symlinks:
588
os.unlink('po/POTFILES.in')
594
class build_kdeui_auto(build_kdeui.build_kdeui):
595
def finalize_options(self):
598
# add *.ui files which belong to KDE4
600
for f in src_fileglob(src, '*.ui'):
602
# might be on the first or second line
603
if fd.readline().startswith('<ui version="') or \
604
fd.readline().startswith('<ui version="'):
606
kdeui_files.append(f)
610
uf = eval(self.ui_files)
614
self.ui_files = repr(uf)
616
build_kdeui.build_kdeui.finalize_options(self)
622
class sdist_auto(distutils.command.sdist.sdist):
623
'''Default values for the 'sdist' command.
625
Replace the manually maintained MANIFEST.in file by providing information
626
about what the source tarball created using the 'sdist' command should
627
contain in normal cases.
629
It prevents the 'build' directory, version control related files, as well as
630
compiled Python and gettext files and temporary files from being included in
633
It's possible for subclasses to extend the 'filter_prefix' and
634
'filter_suffix' properties.
636
filter_prefix = ['build', '.git', '.svn', '.CVS', '.bzr', '.shelf']
637
filter_suffix = ['.pyc', '.mo', '~', '.swp']
639
def add_defaults(self):
640
distutils.command.sdist.sdist.add_defaults(self)
642
if os.path.exists('MANIFEST.in'):
645
self.filter_prefix.append(os.path.join('dist',
646
self.distribution.get_name()))
648
for f in distutils.filelist.findall():
649
if f in self.filelist.files or \
650
any(map(f.startswith, self.filter_prefix)) or \
651
any(map(f.endswith, self.filter_suffix)):
654
self.filelist.append(f)
657
# Automatic installation of ./etc/ and symlinks
660
class install_auto(distutils.command.install.install):
662
# install files from etc/
663
if os.path.isdir('etc'):
664
# work around a bug in copy_tree() which fails with "File exists" on
665
# previously existing symlinks
666
for f in distutils.filelist.findall('etc'):
667
if not f.startswith('etc' + os.path.sep) or not os.path.islink(f):
670
os.unlink(os.path.join(self.root, f))
675
distutils.dir_util.copy_tree('etc', os.path.join(self.root, 'etc'),
676
preserve_times=0, preserve_symlinks=1, verbose=1)
678
# install data/scripts symlinks
679
for f in distutils.filelist.findall():
680
if not os.path.islink(f):
682
if f.startswith('bin/') or f.startswith('data/'):
683
if f.startswith('bin'):
684
dir = self.install_scripts
685
dest = os.path.join(dir, os.path.sep.join(f.split(os.path.sep)[1:]))
686
elif f.startswith('data/icons'):
687
dir = os.path.join(self.install_data, 'share', 'icons', 'hicolor')
688
dest = os.path.join(dir, os.path.sep.join(f.split(os.path.sep)[2:]))
690
dir = os.path.join(self.install_data, 'share', self.distribution.get_name())
691
dest = os.path.join(dir, os.path.sep.join(f.split(os.path.sep)[1:]))
693
d = os.path.dirname(dest)
694
if not os.path.isdir(d):
696
if os.path.exists(dest):
698
os.symlink(os.readlink(f), dest)
700
distutils.command.install.install.run(self)