"""distutils.command.build_py



Implements the Distutils 'build_py' command."""



# This module should be kept compatible with Python 2.1.



__revision__ = "$Id: build_py.py 65742 2008-08-17 04:16:04Z brett.cannon $"



import string, os

from types import *

from glob import glob



from distutils.core import Command

from distutils.errors import *

from distutils.util import convert_path

from distutils import log



class build_py (Command):



    description = "\"build\" pure Python modules (copy to build directory)"



    user_options = [

        ('build-lib=', 'd', "directory to \"build\" (copy) to"),

        ('compile', 'c', "compile .py to .pyc"),

        ('no-compile', None, "don't compile .py files [default]"),

        ('optimize=', 'O',

         "also compile with optimization: -O1 for \"python -O\", "

         "-O2 for \"python -OO\", and -O0 to disable [default: -O0]"),

        ('force', 'f', "forcibly build everything (ignore file timestamps)"),

        ]



    boolean_options = ['compile', 'force']

    negative_opt = {'no-compile' : 'compile'}





    def initialize_options (self):

        self.build_lib = None

        self.py_modules = None

        self.package = None

        self.package_data = None

        self.package_dir = None

        self.compile = 0

        self.optimize = 0

        self.force = None



    def finalize_options (self):

        self.set_undefined_options('build',

                                   ('build_lib', 'build_lib'),

                                   ('force', 'force'))



        # Get the distribution options that are aliases for build_py

        # options -- list of packages and list of modules.

        self.packages = self.distribution.packages

        self.py_modules = self.distribution.py_modules

        self.package_data = self.distribution.package_data

        self.package_dir = {}

        if self.distribution.package_dir:

            for name, path in self.distribution.package_dir.items():

                self.package_dir[name] = convert_path(path)

        self.data_files = self.get_data_files()



        # Ick, copied straight from install_lib.py (fancy_getopt needs a

        # type system!  Hell, *everything* needs a type system!!!)

        if type(self.optimize) is not IntType:

            try:

                self.optimize = int(self.optimize)

                assert 0 <= self.optimize <= 2

            except (ValueError, AssertionError):

                raise DistutilsOptionError, "optimize must be 0, 1, or 2"



    def run (self):



        # XXX copy_file by default preserves 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 by default preserves mode, which appears to be the

        # wrong thing to do: if a file is read-only in the working

        # directory, we want it to be installed read/write so that the next

        # installation of the same module distribution can overwrite it

        # without problems.  (This might be a Unix-specific issue.)  Thus

        # we turn off 'preserve_mode' when copying to the build directory,

        # since the build directory is supposed to be exactly what the

        # installation will look like (ie. we preserve mode when

        # installing).



        # Two options control which modules will be installed: 'packages'

        # and 'py_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.



        if self.py_modules:

            self.build_modules()

        if self.packages:

            self.build_packages()

            self.build_package_data()



        self.byte_compile(self.get_outputs(include_bytecode=0))



    # run ()



    def get_data_files (self):

        """Generate list of '(package,src_dir,build_dir,filenames)' tuples"""

        data = []

        if not self.packages:

            return data

        for package in self.packages:

            # Locate package source directory

            src_dir = self.get_package_dir(package)



            # Compute package build directory

            build_dir = os.path.join(*([self.build_lib] + package.split('.')))



            # Length of path to strip from found files

            plen = 0

            if src_dir:

                plen = len(src_dir)+1



            # Strip directory from globbed filenames

            filenames = [

                file[plen:] for file in self.find_data_files(package, src_dir)

                ]

            data.append((package, src_dir, build_dir, filenames))

        return data



    def find_data_files (self, package, src_dir):

        """Return filenames for package's data files in 'src_dir'"""

        globs = (self.package_data.get('', [])

                 + self.package_data.get(package, []))

        files = []

        for pattern in globs:

            # Each pattern has to be converted to a platform-specific path

            filelist = glob(os.path.join(src_dir, convert_path(pattern)))

            # Files that match more than one pattern are only added once

            files.extend([fn for fn in filelist if fn not in files])

        return files



    def build_package_data (self):

        """Copy data files into build directory"""

        lastdir = None

        for package, src_dir, build_dir, filenames in self.data_files:

            for filename in filenames:

                target = os.path.join(build_dir, filename)

                self.mkpath(os.path.dirname(target))

                self.copy_file(os.path.join(src_dir, filename), target,

                               preserve_mode=False)



    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)."""



        path = string.split(package, '.')



        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 os.path.join(*tail)

            else:

                # Oops, got all the way through 'path' without finding a

                # match in package_dir.  If package_dir defines a directory

                # for the root (nameless) package, then fallback on it;

                # otherwise, we might as well have not consulted

                # package_dir at all, as we just use the directory implied

                # by 'tail' (which should be the same as the original value

                # of 'path' at this point).

                pdir = self.package_dir.get('')

                if pdir is not None:

                    tail.insert(0, pdir)



                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 DistutilsFileError, \

                      ("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 os.path.isfile(init_py):

                return init_py

            else:

                log.warn(("package init file '%s' not found " +

                          "(or not a regular file)"), init_py)



        # Either not in a package at all (__init__.py not expected), or

        # __init__.py doesn't exist -- so don't return the filename.

        return None



    # check_package ()





    def check_module (self, module, module_file):

        if not os.path.isfile(module_file):

            log.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):

        self.check_package(package, package_dir)

        module_files = glob(os.path.join(package_dir, "*.py"))

        modules = []

        setup_script = os.path.abspath(self.distribution.script_name)



        for f in module_files:

            abs_f = os.path.abspath(f)

            if abs_f != setup_script:

                module = os.path.splitext(os.path.basename(f))[0]

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

            else:

                self.debug_print("excluding %s" % setup_script)

        return modules





    def find_modules (self):

        """Finds individually-specified Python modules, ie. those listed by

        module name in 'self.py_modules'.  Returns a list of tuples (package,

        module_base, filename): 'package' is a tuple of the path through

        package-space to the module; 'module_base' is the bare (no

        packages, no dots) module name, and 'filename' is the path to the

        ".py" file (relative to the distribution root) that implements the

        module.

        """



        # 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 (package, module, 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.py_modules:

            path = string.split(module, '.')

            package = string.join(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:

                init_py = self.check_package(package, package_dir)

                packages[package] = (package_dir, 1)

                if init_py:

                    modules.append((package, "__init__", init_py))



            # 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((package, module_base, module_file))



        return modules



    # find_modules ()





    def find_all_modules (self):

        """Compute the list of all modules that will be built, whether

        they are specified one-module-at-a-time ('self.py_modules') or

        by whole packages ('self.packages').  Return a list of tuples

        (package, module, module_file), just like 'find_modules()' and

        'find_package_modules()' do."""



        modules = []

        if self.py_modules:

            modules.extend(self.find_modules())

        if self.packages:

            for package in self.packages:

                package_dir = self.get_package_dir(package)

                m = self.find_package_modules(package, package_dir)

                modules.extend(m)



        return modules



    # find_all_modules ()





    def get_source_files (self):



        modules = self.find_all_modules()

        filenames = []

        for module in modules:

            filenames.append(module[-1])



        return filenames





    def get_module_outfile (self, build_dir, package, module):

        outfile_path = [build_dir] + list(package) + [module + ".py"]

        return os.path.join(*outfile_path)





    def get_outputs (self, include_bytecode=1):

        modules = self.find_all_modules()

        outputs = []

        for (package, module, module_file) in modules:

            package = string.split(package, '.')

            filename = self.get_module_outfile(self.build_lib, package, module)

            outputs.append(filename)

            if include_bytecode:

                if self.compile:

                    outputs.append(filename + "c")

                if self.optimize > 0:

                    outputs.append(filename + "o")



        outputs += [

            os.path.join(build_dir, filename)

            for package, src_dir, build_dir, filenames in self.data_files

            for filename in filenames

            ]



        return outputs





    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_lib (the build

        # directory for Python source).

        outfile = self.get_module_outfile(self.build_lib, package, module)

        dir = os.path.dirname(outfile)

        self.mkpath(dir)

        return self.copy_file(module_file, outfile, preserve_mode=0)





    def build_modules (self):



        modules = self.find_modules()

        for (package, module, module_file) in modules:



            # Now "build" the module -- ie. copy the source file to

            # self.build_lib (the build directory for Python source).

            # (Actually, it gets copied to the directory for this package

            # under self.build_lib.)

            self.build_module(module, module_file, package)



    # build_modules ()





    def build_packages (self):



        for package in self.packages:



            # Get list of (package, module, module_file) tuples based on

            # scanning the package directory.  'package' is only included

            # in the tuple so that 'find_modules()' and

            # 'find_package_tuples()' have a consistent interface; it's

            # ignored here (apart from a sanity check).  Also, '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').

            package_dir = self.get_package_dir(package)

            modules = self.find_package_modules(package, package_dir)



            # Now loop over the modules we found, "building" each one (just

            # copy it to self.build_lib).

            for (package_, module, module_file) in modules:

                assert package == package_

                self.build_module(module, module_file, package)



    # build_packages ()





    def byte_compile (self, files):

        from distutils.util import byte_compile

        prefix = self.build_lib

        if prefix[-1] != os.sep:

            prefix = prefix + os.sep



        # XXX this code is essentially the same as the 'byte_compile()

        # method of the "install_lib" command, except for the determination

        # of the 'prefix' string.  Hmmm.



        if self.compile:

            byte_compile(files, optimize=0,

                         force=self.force, prefix=prefix, dry_run=self.dry_run)

        if self.optimize > 0:

            byte_compile(files, optimize=self.optimize,

                         force=self.force, prefix=prefix, dry_run=self.dry_run)



# class build_py

