summaryrefslogtreecommitdiffstats
path: root/Lib/distutils/command/build_py.py
blob: b3cc1e95cc6b1851976ca0cc9edb117910508c4e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
"""distutils.command.build_py

Implements the Distutils 'build_py' command."""

# created 1999/03/08, Greg Ward

__rcsid__ = "$Id$"

import string, os
from types import *
from glob import glob

from distutils.core import Command
from distutils.errors import *
from distutils.util import mkpath, copy_file


class BuildPy (Command):

    options = [('build-dir=', 'd', "directory for platform-shared files"),
              ]


    def set_default_options (self):
        self.build_dir = None
        self.modules = None
        self.package = None
        self.package_dir = None

    def set_final_options (self):
        self.set_undefined_options ('build',
                                    ('build_lib', 'build_dir'))

        # Get the distribution options that are aliases for build_py
        # options -- list of packages and list of modules.
        self.packages = self.distribution.packages
        self.modules = self.distribution.py_modules
        self.package_dir = self.distribution.package_dir


    def run (self):

        # XXX copy_file by default preserves all stat info -- mode, atime,
        # and mtime.  IMHO this is the right thing to do, but perhaps it
        # should be an option -- in particular, a site administrator might
        # want installed files to reflect the time of installation rather
        # than the last modification time before the installed release.

        # XXX copy_file does *not* preserve MacOS-specific file metadata.
        # If this is a problem for building/installing Python modules, then
        # we'll have to fix copy_file.  (And what about installing scripts,
        # when the time comes for that -- does MacOS use its special
        # metadata to know that a file is meant to be interpreted by
        # Python?)

        infiles = []
        outfiles = []
        missing = []

        # Two options control which modules will be installed: 'packages'
        # and 'modules'.  The former lets us work with whole packages, not
        # specifying individual modules at all; the latter is for
        # specifying modules one-at-a-time.  Currently they are mutually
        # exclusive: you can define one or the other (or neither), but not
        # both.  It remains to be seen how limiting this is.

        # Dispose of the two "unusual" cases first: no pure Python modules
        # at all (no problem, just return silently), and over-specified
        # 'packages' and 'modules' options.

        if not self.modules and not self.packages:
            return
        if self.modules and self.packages:
            raise DistutilsOptionError, \
                  "build_py: supplying both 'packages' and 'modules' " + \
                  "options not allowed"

        # Now we're down to two cases: 'modules' only and 'packages' only.
        if self.modules:
            self.build_modules ()
        else:
            self.build_packages ()


    # run ()
        

    def get_package_dir (self, package):
        """Return the directory, relative to the top of the source
           distribution, where package 'package' should be found
           (at least according to the 'package_dir' option, if any)."""

        if type (package) is StringType:
            path = string.split (package, '.')
        elif type (package) in (TupleType, ListType):
            path = list (package)
        else:
            raise TypeError, "'package' must be a string, list, or tuple"

        if not self.package_dir:
            if path:
                return apply (os.path.join, path)
            else:
                return ''
        else:
            tail = []
            while path:
                try:
                    pdir = self.package_dir[string.join (path, '.')]
                except KeyError:
                    tail.insert (0, path[-1])
                    del path[-1]
                else:
                    tail.insert (0, pdir)
                    return apply (os.path.join, tail)
            else:
                # arg! everything failed, we might as well have not even
                # looked in package_dir -- oh well
                if tail:
                    return apply (os.path.join, tail)
                else:
                    return ''

    # get_package_dir ()


    def check_package (self, package, package_dir):

        # Empty dir name means current directory, which we can probably
        # assume exists.  Also, os.path.exists and isdir don't know about
        # my "empty string means current dir" convention, so we have to
        # circumvent them.
        if package_dir != "":
            if not os.path.exists (package_dir):
                raise DistutilsFileError, \
                      "package directory '%s' does not exist" % package_dir
            if not os.path.isdir (package_dir):
                raise DistutilsFileErorr, \
                      ("supposed package directory '%s' exists, " +
                       "but is not a directory") % package_dir

        # Require __init__.py for all but the "root package"
        if package:
            init_py = os.path.join (package_dir, "__init__.py")
            if not os.path.isfile (init_py):
                self.warn (("package init file '%s' not found " +
                            "(or not a regular file)") % init_py)
    # check_package ()


    def check_module (self, module, module_file):
        if not os.path.isfile (module_file):
            self.warn ("file %s (for module %s) not found" %
                       module_file, module)
            return 0
        else:
            return 1

    # check_module ()


    def find_package_modules (self, package, package_dir):
        module_files = glob (os.path.join (package_dir, "*.py"))
        module_pairs = []
        for f in module_files:
            module = os.path.splitext (os.path.basename (f))[0]
            module_pairs.append (module, f)
        return module_pairs


    def find_modules (self):
        # Map package names to tuples of useful info about the package:
        #    (package_dir, checked)
        # package_dir - the directory where we'll find source files for
        #   this package
        # checked - true if we have checked that the package directory
        #   is valid (exists, contains __init__.py, ... ?)
        packages = {}

        # List of (module, package, filename) tuples to return
        modules = []

        # We treat modules-in-packages almost the same as toplevel modules,
        # just the "package" for a toplevel is empty (either an empty
        # string or empty list, depending on context).  Differences:
        #   - don't check for __init__.py in directory for empty package

        for module in self.modules:
            path = string.split (module, '.')
            package = tuple (path[0:-1])
            module_base = path[-1]

            try:
                (package_dir, checked) = packages[package]
            except KeyError:
                package_dir = self.get_package_dir (package)
                checked = 0

            if not checked:
                self.check_package (package, package_dir)
                packages[package] = (package_dir, 1)

            # XXX perhaps we should also check for just .pyc files
            # (so greedy closed-source bastards can distribute Python
            # modules too)
            module_file = os.path.join (package_dir, module_base + ".py")
            if not self.check_module (module, module_file):
                continue

            modules.append ((module, package, module_file))

        return modules

    # find_modules ()


    def get_source_files (self):

        if self.modules:
            modules = self.find_modules ()
        else:
            modules = []
            for package in self.packages:
                package_dir = self.get_package_dir (package)
                m = self.find_package_modules (package, package_dir)
                modules.extend (m)

        # Both find_modules() and find_package_modules() return a list of
        # tuples where the last element of each tuple is the filename --
        # what a happy coincidence!
        filenames = []
        for module in modules:
            filenames.append (module[-1])

        return filenames                


    def build_module (self, module, module_file, package):

        if type (package) is StringType:
            package = string.split (package, '.')
        elif type (package) not in (ListType, TupleType):
            raise TypeError, \
                  "'package' must be a string (dot-separated), list, or tuple"

        # Now put the module source file into the "build" area -- this is
        # easy, we just copy it somewhere under self.build_dir (the build
        # directory for Python source).
        outfile_path = list (package)
        outfile_path.append (module + ".py")
        outfile_path.insert (0, self.build_dir)
        outfile = apply (os.path.join, outfile_path)

        dir = os.path.dirname (outfile)
        self.mkpath (dir)
        self.copy_file (module_file, outfile)


    def build_modules (self):

        modules = self.find_modules()
        for (module, package, module_file) in modules:

            # Now "build" the module -- ie. copy the source file to
            # self.build_dir (the build directory for Python source).
            # (Actually, it gets copied to the directory for this package
            # under self.build_dir.)
            self.build_module (module, module_file, package)

    # build_modules ()


    def build_packages (self):

        for package in self.packages:
            package_dir = self.get_package_dir (package)
            self.check_package (package, package_dir)

            # Get list of (module, module_file) tuples based on scanning
            # the package directory.  Here, 'module' is the *unqualified*
            # module name (ie. no dots, no package -- we already know its
            # package!), and module_file is the path to the .py file,
            # relative to the current directory (ie. including
            # 'package_dir').
            modules = self.find_package_modules (package, package_dir)

            # Now loop over the modules we found, "building" each one (just
            # copy it to self.build_dir).
            for (module, module_file) in modules:
                self.build_module (module, module_file, package)

    # build_packages ()
                       
# end class BuildPy