~barry/ubuntu/natty/python-distutils-extra/670188-ftbfs

« back to all changes in this revision

Viewing changes to DistUtilsExtra/auto.py

  • Committer: Bazaar Package Importer
  • Author(s): Martin Pitt
  • Date: 2009-07-01 16:39:54 UTC
  • mfrom: (1.1.13 karmic)
  • Revision ID: james.westby@ubuntu.com-20090701163954-3ewvhmu8l9oci2w9
Tags: 2.3
* auto.py: Fix recognition of GtkBuilder *.ui files as glade-3 writes them.
* auto.py: Add automatic calculation of "requires" unless explicitly given.
* auto.py: Add automatic calculation of "provides" unless explicitly given.
* Drop test/testBzrBuild.py, it's specific to Sebastian's computer.
* setup.py: Drop nose.collector, we don't use it.
* Add debian/local/python-mkdebian: Create/update debian packaging
  (debian/*) from python egg-info data. Not terribly pretty, but working and
  reasonably policy compliant.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
'''DistUtilsExtra.auto
 
2
 
 
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.
 
7
 
 
8
This currently supports:
 
9
 
 
10
 * Python modules (./*.py, only in root directory)
 
11
 * Python packages (all directories with __init__.py)
 
12
 * GtkBuilder (*.ui)
 
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
 
22
   revision control)
 
23
 * manpages (*.[0-9])
 
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
 
27
 
 
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.
 
31
'''
 
32
 
 
33
__version__ = '2.3'
 
34
 
 
35
# (c) 2009 Canonical Ltd.
 
36
# Author: Martin Pitt <martin.pitt@ubuntu.com>
 
37
 
 
38
import os, os.path, fnmatch, stat, sys
 
39
import compiler # TODO: deprecated
 
40
import distutils.core
 
41
 
 
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
 
48
 
 
49
# FIXME: global variable, to share with build_i18n_auto
 
50
src = {}
 
51
src_all = {}
 
52
 
 
53
def setup(**attrs):
 
54
    '''Auto-inferring extension of standard distutils.core.setup()'''
 
55
    global src
 
56
    global src_all
 
57
    src_all = src_find(attrs)
 
58
    src = src_all.copy()
 
59
 
 
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', [])))
 
63
 
 
64
    src_mark(src, 'setup.py')
 
65
 
 
66
    # mark files in etc/*, handled by install_auto
 
67
    for f in src.copy():
 
68
        if f.startswith('etc' + os.path.sep):
 
69
            src.remove(f)
 
70
 
 
71
    __cmdclass(attrs)
 
72
    __modules(attrs, src)
 
73
    __packages(attrs, src)
 
74
    __provides(attrs, src)
 
75
    __dbus(attrs, src)
 
76
    __data(attrs, src)
 
77
    __scripts(attrs, src)
 
78
    __stdfiles(attrs, src)
 
79
    __gtkbuilder(attrs, src)
 
80
    __manpages(attrs, src)
 
81
 
 
82
    if 'clean' not in sys.argv:
 
83
        __requires(attrs, src_all)
 
84
 
 
85
    distutils.core.setup(**attrs)
 
86
 
 
87
    if src:
 
88
        print 'WARNING: the following files are not recognized by DistUtilsExtra.auto:'
 
89
        for f in sorted(src):
 
90
            print ' ', f
 
91
 
 
92
#
 
93
# parts of setup()
 
94
#
 
95
 
 
96
class clean_build_tree(distutils.command.clean.clean):
 
97
 
 
98
    description = 'clean up build/ directory'
 
99
 
 
100
    def run(self):
 
101
        # clean build/mo
 
102
        if os.path.isdir('build'):
 
103
            distutils.dir_util.remove_tree('build')
 
104
        distutils.command.clean.clean.run(self)
 
105
 
 
106
def __cmdclass(attrs):
 
107
    '''Default cmdclass for DistUtilsExtra'''
 
108
 
 
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)
 
117
 
 
118
def __modules(attrs, src):
 
119
    '''Default modules'''
 
120
 
 
121
    if 'py_modules' in attrs:
 
122
        for mod in attrs['py_modules']:
 
123
            src_markglob(src, os.path.join(mod, '*.py'))
 
124
        return
 
125
 
 
126
    mods = attrs.setdefault('py_modules', [])
 
127
 
 
128
    for f in src_fileglob(src, '*.py'):
 
129
        if os.path.sep not in f:
 
130
            mods.append(os.path.splitext(f)[0])
 
131
            src_markglob(src, f)
 
132
 
 
133
def __packages(attrs, src):
 
134
    '''Default packages'''
 
135
 
 
136
    if 'packages' in attrs:
 
137
        for pkg in attrs['packages']:
 
138
            src_markglob(src, os.path.join(pkg, '*.py'))
 
139
        return
 
140
 
 
141
    packages = attrs.setdefault('packages', [])
 
142
 
 
143
    for f in src_fileglob(src, '__init__.py'):
 
144
        pkg = os.path.dirname(f)
 
145
        packages.append(pkg)
 
146
        src_markglob(src, os.path.join(pkg, '*.py'))
 
147
 
 
148
def __dbus(attrs, src):
 
149
    '''D-Bus configuration and services'''
 
150
 
 
151
    v = attrs.setdefault('data_files', [])
 
152
 
 
153
    # /etc/dbus-1/system.d/*.conf
 
154
    dbus_conf = []
 
155
    for f in src_fileglob(src, '*.conf'):
 
156
        if '-//freedesktop//DTD D-BUS Bus Configuration' in open(f).read():
 
157
            src_mark(src, f)
 
158
            dbus_conf.append(f)
 
159
    if dbus_conf:
 
160
        v.append(('/etc/dbus-1/system.d/', dbus_conf))
 
161
 
 
162
    session_service = []
 
163
    system_service = []
 
164
    # dbus services
 
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:
 
168
            continue
 
169
        for l in lines:
 
170
            if l.startswith('User='):
 
171
                src_mark(src, f)
 
172
                system_service.append(f)
 
173
                break
 
174
        else:
 
175
            src_mark(src, f)
 
176
            session_service.append(f)
 
177
    if system_service:
 
178
        v.append(('share/dbus-1/system-services', system_service))
 
179
    if session_service:
 
180
        v.append(('share/dbus-1/services', session_service))
 
181
 
 
182
def __data(attrs, src):
 
183
    '''Install auxiliary data files.
 
184
 
 
185
    This installs everything from data/ except data/icons/ into
 
186
    prefix/share/<projectname>/.
 
187
    '''
 
188
    v = attrs.setdefault('data_files', [])
 
189
 
 
190
    assert 'name' in attrs, 'You need to set the "name" property in setup.py'
 
191
 
 
192
    data_files = []
 
193
    for f in src.copy():
 
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]))
 
196
            src_mark(src, f)
 
197
 
 
198
def __scripts(attrs, src):
 
199
    '''Install scripts.
 
200
 
 
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.
 
204
    '''
 
205
    assert 'name' in attrs, 'You need to set the "name" property in setup.py'
 
206
 
 
207
    scripts = []
 
208
    for f in src.copy():
 
209
        if f.startswith('bin/') or f == attrs['name']:
 
210
            st = os.stat(f)
 
211
            if stat.S_ISREG(st.st_mode) and st.st_mode & stat.S_IEXEC:
 
212
                scripts.append(f)
 
213
                src_mark(src, f)
 
214
 
 
215
    if scripts:
 
216
        v = attrs.setdefault('scripts', [])
 
217
        v += scripts
 
218
 
 
219
def __stdfiles(attrs, src):
 
220
    '''Install/mark standard files.
 
221
 
 
222
    This covers COPYING, AUTHORS, README, etc.
 
223
    '''
 
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')
 
230
 
 
231
    # install all README* from the root directory
 
232
    readme = []
 
233
    for f in src_fileglob(src, 'README*').union(src_fileglob(src, 'NEWS')):
 
234
        if os.path.sep not in f:
 
235
            readme.append(f)
 
236
            src_mark(src, f)
 
237
    if readme:
 
238
        assert 'name' in attrs, 'You need to set the "name" property in setup.py'
 
239
 
 
240
        attrs.setdefault('data_files', []).append((os.path.join('share', 'doc',
 
241
            attrs['name']), readme))
 
242
 
 
243
def __gtkbuilder(attrs, src):
 
244
    '''Install GtkBuilder *.ui files'''
 
245
 
 
246
    ui = []
 
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:
 
250
            src_mark(src, f)
 
251
            ui.append(f)
 
252
    if ui:
 
253
        assert 'name' in attrs, 'You need to set the "name" property in setup.py'
 
254
 
 
255
        attrs.setdefault('data_files', []).append((os.path.join('share', 
 
256
            attrs['name']), ui))
 
257
 
 
258
def __manpages(attrs, src):
 
259
    '''Install manpages'''
 
260
 
 
261
    mans = {}
 
262
    for f in src_fileglob(src_all, '*.[0123456789]'):
 
263
        line = open(f).readline()
 
264
        if line.startswith('.TH '):
 
265
            src_mark(src, f)
 
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))
 
270
 
 
271
def __external_mod(module, attrs):
 
272
    '''Check if given Python module is not included in Python or locally'''
 
273
 
 
274
    # filter out locally provided modules
 
275
    if module in attrs['provides']:
 
276
        return False
 
277
    for m in _module_parents(module):
 
278
        if m in attrs['provides']:
 
279
            return False
 
280
 
 
281
    try:
 
282
        path = __import__(module).__file__
 
283
    except ImportError:
 
284
        print >> sys.stderr, 'ERROR: Python module %s not found' % module
 
285
        return False
 
286
    except AttributeError: # builtin modules
 
287
        return False
 
288
 
 
289
    return 'dist-packages' in path or 'site-packages' in path or \
 
290
            not path.startswith(os.path.dirname(os.__file__))
 
291
 
 
292
def __add_imports(imports, file, attrs):
 
293
    '''Add all imported modules from file to imports set.
 
294
 
 
295
    This filters out modules which are shipped with Python itself.
 
296
    '''
 
297
    try:
 
298
        ast = compiler.parseFile(file)
 
299
 
 
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):
 
304
                        imports.add(name)
 
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
 
310
 
 
311
def _module_parents(mod):
 
312
    '''Iterate over all parents of a module'''
 
313
 
 
314
    hierarchy = mod.split('.')
 
315
    hierarchy.pop()
 
316
    while hierarchy:
 
317
        yield '.'.join(hierarchy)
 
318
        hierarchy.pop()
 
319
 
 
320
def __filter_namespace(modules):
 
321
    '''Filter out modules which are already covered by a parent module
 
322
    
 
323
    E. g. this transforms ['os.path', 'os', 'foo.bar.baz', 'foo.bar'] to
 
324
    ['os', 'foo.bar'].
 
325
    '''
 
326
    result = set()
 
327
 
 
328
    for m in modules:
 
329
        for p in _module_parents(m):
 
330
            if p in modules:
 
331
                break
 
332
        else:
 
333
            result.add(m)
 
334
 
 
335
    return sorted(result)
 
336
 
 
337
def __requires(attrs, src_all):
 
338
    '''Determine requires (if not set explicitly)'''
 
339
 
 
340
    if 'requires' in attrs:
 
341
        return
 
342
 
 
343
    imports = set()
 
344
 
 
345
    # iterate over all *.py and scripts which are Python
 
346
    for s in src_all:
 
347
        ext = os.path.splitext(s)[1]
 
348
        if ext == '':
 
349
            f = open(s)
 
350
            line = f.readline()
 
351
            if not line.startswith('#!') or 'python' not in line:
 
352
                continue
 
353
        elif ext != '.py':
 
354
            continue
 
355
        __add_imports(imports, s, attrs)
 
356
 
 
357
    attrs['requires'] = __filter_namespace(imports)
 
358
 
 
359
def __provides(attrs, src_all):
 
360
    '''Determine provides (if not set explicitly)'''
 
361
 
 
362
    if 'provides' in attrs:
 
363
        return
 
364
 
 
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)
 
369
 
 
370
#
 
371
# helper functions
 
372
#
 
373
 
 
374
def src_find(attrs):
 
375
    '''Find source files.
 
376
    
 
377
    This ignores all source files which are explicitly specified as setup()
 
378
    arguments.
 
379
    '''
 
380
    src = set()
 
381
 
 
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)
 
386
 
 
387
    for (root, dirs, files) in os.walk('.'):
 
388
        if root.startswith('./'):
 
389
            root = root[2:]
 
390
        if root == '.':
 
391
            root = ''
 
392
        if root.startswith('.') or \
 
393
                root.split(os.path.sep, 1)[0] in ('build', 'test', 'tests'):
 
394
            continue
 
395
        # data/icons is handled by build_icons
 
396
        if root.startswith(os.path.join('data', 'icons')):
 
397
            continue
 
398
        for f in files:
 
399
            ext = os.path.splitext(f)[1]
 
400
            if f.startswith('.') or ext in ('.pyc', '~', '.mo'):
 
401
                continue
 
402
            # po/*.po is taken care of by build_i18n
 
403
            if root == 'po' and (ext == '.po' or f == 'POTFILES.in'):
 
404
                continue
 
405
            
 
406
            path = os.path.join(root, f)
 
407
            if path not in explicit:
 
408
                src.add(path)
 
409
 
 
410
    return src
 
411
 
 
412
def src_fileglob(src, fnameglob):
 
413
    '''Return set of files which match fnameglob.'''
 
414
 
 
415
    result = set()
 
416
    for f in src:
 
417
        if fnmatch.fnmatch(os.path.basename(f), fnameglob):
 
418
            result.add(f)
 
419
    return result
 
420
 
 
421
def src_mark(src, path):
 
422
    '''Remove path from src.'''
 
423
 
 
424
    src.remove(path)
 
425
 
 
426
def src_markglob(src, pathglob):
 
427
    '''Remove all paths from src which match pathglob.'''
 
428
 
 
429
    for f in src.copy():
 
430
        if fnmatch.fnmatch(f, pathglob):
 
431
            src.remove(f)
 
432
 
 
433
#
 
434
# Automatic setup.cfg
 
435
#
 
436
 
 
437
class build_i18n_auto(build_i18n.build_i18n):
 
438
    def finalize_options(self):
 
439
        build_i18n.build_i18n.finalize_options(self)
 
440
        global src
 
441
 
 
442
        # add PolicyKit files
 
443
        policy_files = []
 
444
        for f in src_fileglob(src, '*.policy.in'):
 
445
            src_mark(src, f)
 
446
            policy_files.append(f)
 
447
        if policy_files:
 
448
            try:
 
449
                xf = eval(self.xml_files)
 
450
            except TypeError:
 
451
                xf = []
 
452
            xf.append(('share/PolicyKit/policy', policy_files))
 
453
            self.xml_files = repr(xf)
 
454
 
 
455
        # add desktop files
 
456
        desktop_files = []
 
457
        autostart_files = []
 
458
        notify_files = []
 
459
        for f in src_fileglob(src, '*.desktop.in'):
 
460
            src_mark(src, f)
 
461
            if 'autostart' in f:
 
462
                autostart_files.append(f)
 
463
            else:
 
464
                desktop_files.append(f)
 
465
        for f in src_fileglob(src, '*.notifyrc.in'):
 
466
            src_mark(src, f)
 
467
            notify_files.append(f)
 
468
        try:
 
469
            df = eval(self.desktop_files)
 
470
        except TypeError:
 
471
            df = []
 
472
        if desktop_files:
 
473
            df.append(('share/applications', desktop_files))
 
474
        if autostart_files:
 
475
            df.append(('share/autostart', autostart_files))
 
476
        if notify_files:
 
477
            df.append(('share/kde4/apps/' + self.distribution.get_name(), notify_files))
 
478
        self.desktop_files = repr(df)
 
479
 
 
480
        # mark PO template as known to handle
 
481
        try:
 
482
            src_mark(src, os.path.join('po', self.distribution.get_name() + '.pot'))
 
483
        except KeyError:
 
484
            pass
 
485
 
 
486
    def run(self):
 
487
        '''Build a default POTFILES.in'''
 
488
 
 
489
        auto_potfiles_in = False
 
490
        exe_symlinks = []
 
491
        global src_all
 
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'))
 
497
 
 
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)
 
502
 
 
503
            # find extensionless executable scripts which are Python files, and
 
504
            # generate a temporary *.py alias, so that they get caught by
 
505
            # intltool
 
506
            for f in src_all:
 
507
                f_py = f + '.py'
 
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)
 
513
                        files.add(f_py)
 
514
                        exe_symlinks.append(f_py)
 
515
 
 
516
            if files:
 
517
                if not os.path.isdir('po'):
 
518
                    os.mkdir('po')
 
519
                potfiles_in = open('po/POTFILES.in', 'w')
 
520
                print >> potfiles_in, '[encoding: UTF-8]'
 
521
                for f in files:
 
522
                    print >> potfiles_in, f
 
523
                potfiles_in.close()
 
524
 
 
525
                auto_potfiles_in = True
 
526
 
 
527
        build_i18n.build_i18n.run(self)
 
528
 
 
529
        for f in exe_symlinks:
 
530
            os.unlink(f)
 
531
 
 
532
        if auto_potfiles_in:
 
533
            os.unlink('po/POTFILES.in')
 
534
            try:
 
535
                os.rmdir('po')
 
536
            except:
 
537
                pass
 
538
 
 
539
class build_kdeui_auto(build_kdeui.build_kdeui):
 
540
    def finalize_options(self):
 
541
        global src
 
542
 
 
543
        # add *.ui files which belong to KDE4
 
544
        kdeui_files = []
 
545
        for f in src_fileglob(src, '*.ui'):
 
546
            if open(f).readline().startswith('<ui version="'):
 
547
                src_mark(src, f)
 
548
                kdeui_files.append(f)
 
549
        if kdeui_files:
 
550
            try:
 
551
                uf = eval(self.ui_files)
 
552
            except TypeError:
 
553
                uf = []
 
554
            uf += kdeui_files
 
555
            self.ui_files = repr(uf)
 
556
 
 
557
        build_kdeui.build_kdeui.finalize_options(self)
 
558
 
 
559
#
 
560
# Automatic sdist
 
561
#
 
562
 
 
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']
 
568
 
 
569
        distutils.command.sdist.sdist.add_defaults(self)
 
570
 
 
571
        manifest_in = os.path.join('MANIFEST.in')
 
572
        if os.path.exists(manifest_in):
 
573
            return
 
574
 
 
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)):
 
578
                continue
 
579
            self.filelist.append(f)
 
580
 
 
581
# Automatic installation of ./etc/
 
582
 
 
583
class install_auto(distutils.command.install.install):
 
584
    def run(self):
 
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):
 
591
                    continue
 
592
                try:
 
593
                    os.unlink(os.path.join(self.root, f))
 
594
                except OSError:
 
595
                    pass
 
596
 
 
597
            distutils.dir_util.copy_tree('etc', os.path.join(self.root, 'etc'),
 
598
                    preserve_times=0, preserve_symlinks=1, verbose=1)
 
599
 
 
600
        distutils.command.install.install.run(self)
 
601