diff --git a/distutils/__init__.py b/distutils/__init__.py new file mode 100644 index 00000000..1f23b972 --- /dev/null +++ b/distutils/__init__.py @@ -0,0 +1,11 @@ +"""distutils + +The main package for the Python Module Distribtion Utilities. Normally +used from a setup script as + + from distutils.core import setup + + setup (...) +""" + +__revision__ = "$Id$" diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py new file mode 100644 index 00000000..4a8c1d38 --- /dev/null +++ b/distutils/ccompiler.py @@ -0,0 +1,809 @@ +"""distutils.ccompiler + +Contains CCompiler, an abstract base class that defines the interface +for the Distutils compiler abstraction model.""" + +# created 1999/07/05, Greg Ward + +__revision__ = "$Id$" + +import sys, os +from types import * +from copy import copy +from distutils.errors import * +from distutils.spawn import spawn +from distutils.util import move_file, mkpath, newer_pairwise, newer_group + + +class CCompiler: + """Abstract base class to define the interface that must be implemented + by real compiler abstraction classes. Might have some use as a + place for shared code, but it's not yet clear what code can be + shared between compiler abstraction models for different platforms. + + The basic idea behind a compiler abstraction class is that each + instance can be used for all the compile/link steps in building + a single project. Thus, attributes common to all of those compile + and link steps -- include directories, macros to define, libraries + to link against, etc. -- are attributes of the compiler instance. + To allow for variability in how individual files are treated, + most (all?) of those attributes may be varied on a per-compilation + or per-link basis.""" + + # 'compiler_type' is a class attribute that identifies this class. It + # keeps code that wants to know what kind of compiler it's dealing with + # from having to import all possible compiler classes just to do an + # 'isinstance'. In concrete CCompiler subclasses, 'compiler_type' + # should really, really be one of the keys of the 'compiler_class' + # dictionary (see below -- used by the 'new_compiler()' factory + # function) -- authors of new compiler interface classes are + # responsible for updating 'compiler_class'! + compiler_type = None + + # XXX things not handled by this compiler abstraction model: + # * client can't provide additional options for a compiler, + # e.g. warning, optimization, debugging flags. Perhaps this + # should be the domain of concrete compiler abstraction classes + # (UnixCCompiler, MSVCCompiler, etc.) -- or perhaps the base + # class should have methods for the common ones. + # * can't put output files (object files, libraries, whatever) + # into a separate directory from their inputs. Should this be + # handled by an 'output_dir' attribute of the whole object, or a + # parameter to the compile/link_* methods, or both? + # * can't completely override the include or library searchg + # path, ie. no "cc -I -Idir1 -Idir2" or "cc -L -Ldir1 -Ldir2". + # I'm not sure how widely supported this is even by Unix + # compilers, much less on other platforms. And I'm even less + # sure how useful it is; maybe for cross-compiling, but + # support for that is a ways off. (And anyways, cross + # compilers probably have a dedicated binary with the + # right paths compiled in. I hope.) + # * can't do really freaky things with the library list/library + # dirs, e.g. "-Ldir1 -lfoo -Ldir2 -lfoo" to link against + # different versions of libfoo.a in different locations. I + # think this is useless without the ability to null out the + # library search path anyways. + + + # Subclasses that rely on the standard filename generation methods + # implemented below should override these; see the comment near + # those methods ('object_filenames()' et. al.) for details: + src_extensions = None # list of strings + obj_extension = None # string + static_lib_extension = None + shared_lib_extension = None # string + static_lib_format = None # format string + shared_lib_format = None # prob. same as static_lib_format + exe_extension = None # string + + + def __init__ (self, + verbose=0, + dry_run=0, + force=0): + + self.verbose = verbose + self.dry_run = dry_run + self.force = force + + # 'output_dir': a common output directory for object, library, + # shared object, and shared library files + self.output_dir = None + + # 'macros': a list of macro definitions (or undefinitions). A + # macro definition is a 2-tuple (name, value), where the value is + # either a string or None (no explicit value). A macro + # undefinition is a 1-tuple (name,). + self.macros = [] + + # 'include_dirs': a list of directories to search for include files + self.include_dirs = [] + + # 'libraries': a list of libraries to include in any link + # (library names, not filenames: eg. "foo" not "libfoo.a") + self.libraries = [] + + # 'library_dirs': a list of directories to search for libraries + self.library_dirs = [] + + # 'runtime_library_dirs': a list of directories to search for + # shared libraries/objects at runtime + self.runtime_library_dirs = [] + + # 'objects': a list of object files (or similar, such as explicitly + # named library files) to include on any link + self.objects = [] + + # __init__ () + + + def _find_macro (self, name): + i = 0 + for defn in self.macros: + if defn[0] == name: + return i + i = i + 1 + + return None + + + def _check_macro_definitions (self, definitions): + """Ensures that every element of 'definitions' is a valid macro + definition, ie. either (name,value) 2-tuple or a (name,) + tuple. Do nothing if all definitions are OK, raise + TypeError otherwise.""" + + for defn in definitions: + if not (type (defn) is TupleType and + (len (defn) == 1 or + (len (defn) == 2 and + (type (defn[1]) is StringType or defn[1] is None))) and + type (defn[0]) is StringType): + raise TypeError, \ + ("invalid macro definition '%s': " % defn) + \ + "must be tuple (string,), (string, string), or " + \ + "(string, None)" + + + # -- Bookkeeping methods ------------------------------------------- + + def define_macro (self, name, value=None): + """Define a preprocessor macro for all compilations driven by + this compiler object. The optional parameter 'value' should be + a string; if it is not supplied, then the macro will be defined + without an explicit value and the exact outcome depends on the + compiler used (XXX true? does ANSI say anything about this?)""" + + # Delete from the list of macro definitions/undefinitions if + # already there (so that this one will take precedence). + i = self._find_macro (name) + if i is not None: + del self.macros[i] + + defn = (name, value) + self.macros.append (defn) + + + def undefine_macro (self, name): + """Undefine a preprocessor macro for all compilations driven by + this compiler object. If the same macro is defined by + 'define_macro()' and undefined by 'undefine_macro()' the last + call takes precedence (including multiple redefinitions or + undefinitions). If the macro is redefined/undefined on a + per-compilation basis (ie. in the call to 'compile()'), then + that takes precedence.""" + + # Delete from the list of macro definitions/undefinitions if + # already there (so that this one will take precedence). + i = self._find_macro (name) + if i is not None: + del self.macros[i] + + undefn = (name,) + self.macros.append (undefn) + + + def add_include_dir (self, dir): + """Add 'dir' to the list of directories that will be searched + for header files. The compiler is instructed to search + directories in the order in which they are supplied by + successive calls to 'add_include_dir()'.""" + self.include_dirs.append (dir) + + def set_include_dirs (self, dirs): + """Set the list of directories that will be searched to 'dirs' + (a list of strings). Overrides any preceding calls to + 'add_include_dir()'; subsequence calls to 'add_include_dir()' + add to the list passed to 'set_include_dirs()'. This does + not affect any list of standard include directories that + the compiler may search by default.""" + self.include_dirs = copy (dirs) + + + def add_library (self, libname): + """Add 'libname' to the list of libraries that will be included + in all links driven by this compiler object. Note that + 'libname' should *not* be the name of a file containing a + library, but the name of the library itself: the actual filename + will be inferred by the linker, the compiler, or the compiler + abstraction class (depending on the platform). + + The linker will be instructed to link against libraries in the + order they were supplied to 'add_library()' and/or + 'set_libraries()'. It is perfectly valid to duplicate library + names; the linker will be instructed to link against libraries + as many times as they are mentioned.""" + self.libraries.append (libname) + + def set_libraries (self, libnames): + """Set the list of libraries to be included in all links driven + by this compiler object to 'libnames' (a list of strings). + This does not affect any standard system libraries that the + linker may include by default.""" + + self.libraries = copy (libnames) + + + def add_library_dir (self, dir): + """Add 'dir' to the list of directories that will be searched for + libraries specified to 'add_library()' and 'set_libraries()'. + The linker will be instructed to search for libraries in the + order they are supplied to 'add_library_dir()' and/or + 'set_library_dirs()'.""" + self.library_dirs.append (dir) + + def set_library_dirs (self, dirs): + """Set the list of library search directories to 'dirs' (a list + of strings). This does not affect any standard library + search path that the linker may search by default.""" + self.library_dirs = copy (dirs) + + + def add_runtime_library_dir (self, dir): + """Add 'dir' to the list of directories that will be searched for + shared libraries at runtime.""" + self.runtime_library_dirs.append (dir) + + def set_runtime_library_dirs (self, dirs): + """Set the list of directories to search for shared libraries + at runtime to 'dirs' (a list of strings). This does not affect + any standard search path that the runtime linker may search by + default.""" + self.runtime_library_dirs = copy (dirs) + + + def add_link_object (self, object): + """Add 'object' to the list of object files (or analogues, such + as explictly named library files or the output of "resource + compilers") to be included in every link driven by this + compiler object.""" + self.objects.append (object) + + def set_link_objects (self, objects): + """Set the list of object files (or analogues) to be included + in every link to 'objects'. This does not affect any + standard object files that the linker may include by default + (such as system libraries).""" + self.objects = copy (objects) + + + # -- Priviate utility methods -------------------------------------- + # (here for the convenience of subclasses) + + def _fix_compile_args (self, output_dir, macros, include_dirs): + """Typecheck and fix-up some of the arguments to the 'compile()' method, + and return fixed-up values. Specifically: if 'output_dir' is + None, replaces it with 'self.output_dir'; ensures that 'macros' + is a list, and augments it with 'self.macros'; ensures that + 'include_dirs' is a list, and augments it with + 'self.include_dirs'. Guarantees that the returned values are of + the correct type, i.e. for 'output_dir' either string or None, + and for 'macros' and 'include_dirs' either list or None.""" + + if output_dir is None: + output_dir = self.output_dir + elif type (output_dir) is not StringType: + raise TypeError, "'output_dir' must be a string or None" + + if macros is None: + macros = self.macros + elif type (macros) is ListType: + macros = macros + (self.macros or []) + else: + raise TypeError, \ + "'macros' (if supplied) must be a list of tuples" + + if include_dirs is None: + include_dirs = self.include_dirs + elif type (include_dirs) in (ListType, TupleType): + include_dirs = list (include_dirs) + (self.include_dirs or []) + else: + raise TypeError, \ + "'include_dirs' (if supplied) must be a list of strings" + + return (output_dir, macros, include_dirs) + + # _fix_compile_args () + + + def _prep_compile (self, sources, output_dir): + """Determine the list of object files corresponding to 'sources', and + figure out which ones really need to be recompiled. Return a list + of all object files and a dictionary telling which source files can + be skipped.""" + + # Get the list of expected output (object) files + objects = self.object_filenames (sources, + output_dir=output_dir) + + if self.force: + skip_source = {} # rebuild everything + for source in sources: + skip_source[source] = 0 + else: + # Figure out which source files we have to recompile according + # to a simplistic check -- we just compare the source and + # object file, no deep dependency checking involving header + # files. + skip_source = {} # rebuild everything + for source in sources: # no wait, rebuild nothing + skip_source[source] = 1 + + (n_sources, n_objects) = newer_pairwise (sources, objects) + for source in n_sources: # no really, only rebuild what's out-of-date + skip_source[source] = 0 + + return (objects, skip_source) + + # _prep_compile () + + + def _fix_link_args (self, objects, output_dir, + takes_libs=0, libraries=None, library_dirs=None): + """Typecheck and fix up some of the arguments supplied to the + 'link_*' methods and return the fixed values. Specifically: + ensure that 'objects' is a list; if output_dir is None, use + self.output_dir; ensure that 'libraries' and 'library_dirs' are + both lists, and augment them with 'self.libraries' and + 'self.library_dirs'. If 'takes_libs' is true, return a tuple + (objects, output_dir, libraries, library_dirs; else return + (objects, output_dir).""" + + if type (objects) not in (ListType, TupleType): + raise TypeError, \ + "'objects' must be a list or tuple of strings" + objects = list (objects) + + if output_dir is None: + output_dir = self.output_dir + elif type (output_dir) is not StringType: + raise TypeError, "'output_dir' must be a string or None" + + if takes_libs: + if libraries is None: + libraries = self.libraries + elif type (libraries) in (ListType, TupleType): + libraries = list (libraries) + (self.libraries or []) + else: + raise TypeError, \ + "'libraries' (if supplied) must be a list of strings" + + if library_dirs is None: + library_dirs = self.library_dirs + elif type (library_dirs) in (ListType, TupleType): + library_dirs = list (library_dirs) + (self.library_dirs or []) + else: + raise TypeError, \ + "'library_dirs' (if supplied) must be a list of strings" + + return (objects, output_dir, libraries, library_dirs) + else: + return (objects, output_dir) + + # _fix_link_args () + + + def _need_link (self, objects, output_file): + """Return true if we need to relink the files listed in 'objects' to + recreate 'output_file'.""" + + if self.force: + return 1 + else: + if self.dry_run: + newer = newer_group (objects, output_file, missing='newer') + else: + newer = newer_group (objects, output_file) + return newer + + # _need_link () + + + # -- Worker methods ------------------------------------------------ + # (must be implemented by subclasses) + + def compile (self, + sources, + output_dir=None, + macros=None, + include_dirs=None, + debug=0, + extra_preargs=None, + extra_postargs=None): + """Compile one or more C/C++ source files. 'sources' must be + a list of strings, each one the name of a C/C++ source + file. Return a list of object filenames, one per source + filename in 'sources'. Depending on the implementation, + not all source files will necessarily be compiled, but + all corresponding object filenames will be returned. + + If 'output_dir' is given, object files will be put under it, + while retaining their original path component. That is, + "foo/bar.c" normally compiles to "foo/bar.o" (for a Unix + implementation); if 'output_dir' is "build", then it would + compile to "build/foo/bar.o". + + 'macros', if given, must be a list of macro definitions. A + macro definition is either a (name, value) 2-tuple or a (name,) + 1-tuple. The former defines a macro; if the value is None, the + macro is defined without an explicit value. The 1-tuple case + undefines a macro. Later definitions/redefinitions/ + undefinitions take precedence. + + 'include_dirs', if given, must be a list of strings, the + directories to add to the default include file search path for + this compilation only. + + 'debug' is a boolean; if true, the compiler will be instructed + to output debug symbols in (or alongside) the object file(s). + + 'extra_preargs' and 'extra_postargs' are implementation- + dependent. On platforms that have the notion of a command-line + (e.g. Unix, DOS/Windows), they are most likely lists of strings: + extra command-line arguments to prepand/append to the compiler + command line. On other platforms, consult the implementation + class documentation. In any event, they are intended as an + escape hatch for those occasions when the abstract compiler + framework doesn't cut the mustard.""" + + pass + + + def create_static_lib (self, + objects, + output_libname, + output_dir=None, + debug=0): + """Link a bunch of stuff together to create a static library + file. The "bunch of stuff" consists of the list of object + files supplied as 'objects', the extra object files supplied + to 'add_link_object()' and/or 'set_link_objects()', the + libraries supplied to 'add_library()' and/or + 'set_libraries()', and the libraries supplied as 'libraries' + (if any). + + 'output_libname' should be a library name, not a filename; the + filename will be inferred from the library name. 'output_dir' + is the directory where the library file will be put. + + 'debug' is a boolean; if true, debugging information will be + included in the library (note that on most platforms, it is the + compile step where this matters: the 'debug' flag is included + here just for consistency).""" + + pass + + + def link_shared_lib (self, + objects, + output_libname, + output_dir=None, + libraries=None, + library_dirs=None, + debug=0, + extra_preargs=None, + extra_postargs=None): + """Link a bunch of stuff together to create a shared library + file. Similar semantics to 'create_static_lib()', with the + addition of other libraries to link against and directories to + search for them. Also, of course, the type and name of + the generated file will almost certainly be different, as will + the program used to create it. + + 'libraries' is a list of libraries to link against. These are + library names, not filenames, since they're translated into + filenames in a platform-specific way (eg. "foo" becomes + "libfoo.a" on Unix and "foo.lib" on DOS/Windows). However, they + can include a directory component, which means the linker will + look in that specific directory rather than searching all the + normal locations. + + 'library_dirs', if supplied, should be a list of directories to + search for libraries that were specified as bare library names + (ie. no directory component). These are on top of the system + default and those supplied to 'add_library_dir()' and/or + 'set_library_dirs()'. + + 'debug' is as for 'compile()' and 'create_static_lib()', with the + slight distinction that it actually matters on most platforms + (as opposed to 'create_static_lib()', which includes a 'debug' + flag mostly for form's sake). + + 'extra_preargs' and 'extra_postargs' are as for 'compile()' + (except of course that they supply command-line arguments + for the particular linker being used).""" + + pass + + + def link_shared_object (self, + objects, + output_filename, + output_dir=None, + libraries=None, + library_dirs=None, + debug=0, + extra_preargs=None, + extra_postargs=None): + """Link a bunch of stuff together to create a shared object + file. Much like 'link_shared_lib()', except the output filename + is explicitly supplied as 'output_filename'. If 'output_dir' is + supplied, 'output_filename' is relative to it + (i.e. 'output_filename' can provide directory components if + needed).""" + pass + + + def link_executable (self, + objects, + output_progname, + output_dir=None, + libraries=None, + library_dirs=None, + debug=0, + extra_preargs=None, + extra_postargs=None): + """Link a bunch of stuff together to create a binary executable + file. The "bunch of stuff" is as for 'link_shared_lib()'. + 'output_progname' should be the base name of the executable + program--e.g. on Unix the same as the output filename, but + on DOS/Windows ".exe" will be appended.""" + pass + + + + # -- Filename generation methods ----------------------------------- + + # The default implementation of the filename generating methods are + # prejudiced towards the Unix/DOS/Windows view of the world: + # * object files are named by replacing the source file extension + # (eg. .c/.cpp -> .o/.obj) + # * library files (shared or static) are named by plugging the + # library name and extension into a format string, eg. + # "lib%s.%s" % (lib_name, ".a") for Unix static libraries + # * executables are named by appending an extension (possibly + # empty) to the program name: eg. progname + ".exe" for + # Windows + # + # To reduce redundant code, these methods expect to find + # several attributes in the current object (presumably defined + # as class attributes): + # * src_extensions - + # list of C/C++ source file extensions, eg. ['.c', '.cpp'] + # * obj_extension - + # object file extension, eg. '.o' or '.obj' + # * static_lib_extension - + # extension for static library files, eg. '.a' or '.lib' + # * shared_lib_extension - + # extension for shared library/object files, eg. '.so', '.dll' + # * static_lib_format - + # format string for generating static library filenames, + # eg. 'lib%s.%s' or '%s.%s' + # * shared_lib_format + # format string for generating shared library filenames + # (probably same as static_lib_format, since the extension + # is one of the intended parameters to the format string) + # * exe_extension - + # extension for executable files, eg. '' or '.exe' + + def object_filenames (self, + source_filenames, + strip_dir=0, + output_dir=''): + if output_dir is None: output_dir = '' + obj_names = [] + for src_name in source_filenames: + (base, ext) = os.path.splitext (src_name) + if ext not in self.src_extensions: + continue + if strip_dir: + base = os.path.basename (base) + obj_names.append (os.path.join (output_dir, + base + self.obj_extension)) + return obj_names + + # object_filenames () + + + def shared_object_filename (self, + basename, + strip_dir=0, + output_dir=''): + if output_dir is None: output_dir = '' + if strip_dir: + basename = os.path.basename (basename) + return os.path.join (output_dir, basename + self.shared_lib_extension) + + + def library_filename (self, + libname, + lib_type='static', # or 'shared' + strip_dir=0, + output_dir=''): + + if output_dir is None: output_dir = '' + if lib_type not in ("static","shared"): + raise ValueError, "'lib_type' must be \"static\" or \"shared\"" + fmt = getattr (self, lib_type + "_lib_format") + ext = getattr (self, lib_type + "_lib_extension") + + (dir, base) = os.path.split (libname) + filename = fmt % (base, ext) + if strip_dir: + dir = '' + + return os.path.join (output_dir, dir, filename) + + + # -- Utility methods ----------------------------------------------- + + def announce (self, msg, level=1): + if self.verbose >= level: + print msg + + def warn (self, msg): + sys.stderr.write ("warning: %s\n" % msg) + + def spawn (self, cmd): + spawn (cmd, verbose=self.verbose, dry_run=self.dry_run) + + def move_file (self, src, dst): + return move_file (src, dst, verbose=self.verbose, dry_run=self.dry_run) + + def mkpath (self, name, mode=0777): + mkpath (name, mode, self.verbose, self.dry_run) + + +# class CCompiler + + +# Map a platform ('posix', 'nt') to the default compiler type for +# that platform. +default_compiler = { 'posix': 'unix', + 'nt': 'msvc', + } + +# Map compiler types to (module_name, class_name) pairs -- ie. where to +# find the code that implements an interface to this compiler. (The module +# is assumed to be in the 'distutils' package.) +compiler_class = { 'unix': ('unixccompiler', 'UnixCCompiler'), + 'msvc': ('msvccompiler', 'MSVCCompiler'), + } + + +def new_compiler (plat=None, + compiler=None, + verbose=0, + dry_run=0, + force=0): + + """Generate an instance of some CCompiler subclass for the supplied + platform/compiler combination. 'plat' defaults to 'os.name' + (eg. 'posix', 'nt'), and 'compiler' defaults to the default + compiler for that platform. Currently only 'posix' and 'nt' + are supported, and the default compilers are "traditional Unix + interface" (UnixCCompiler class) and Visual C++ (MSVCCompiler + class). Note that it's perfectly possible to ask for a Unix + compiler object under Windows, and a Microsoft compiler object + under Unix -- if you supply a value for 'compiler', 'plat' + is ignored.""" + + if plat is None: + plat = os.name + + try: + if compiler is None: + compiler = default_compiler[plat] + + (module_name, class_name) = compiler_class[compiler] + except KeyError: + msg = "don't know how to compile C/C++ code on platform '%s'" % plat + if compiler is not None: + msg = msg + " with '%s' compiler" % compiler + raise DistutilsPlatformError, msg + + try: + module_name = "distutils." + module_name + __import__ (module_name) + module = sys.modules[module_name] + klass = vars(module)[class_name] + except ImportError: + raise DistutilsModuleError, \ + "can't compile C/C++ code: unable to load module '%s'" % \ + module_name + except KeyError: + raise DistutilsModuleError, \ + ("can't compile C/C++ code: unable to find class '%s' " + + "in module '%s'") % (class_name, module_name) + + return klass (verbose, dry_run, force) + + +def gen_preprocess_options (macros, include_dirs): + """Generate C pre-processor options (-D, -U, -I) as used by at + least two types of compilers: the typical Unix compiler and Visual + C++. 'macros' is the usual thing, a list of 1- or 2-tuples, where + (name,) means undefine (-U) macro 'name', and (name,value) means + define (-D) macro 'name' to 'value'. 'include_dirs' is just a list of + directory names to be added to the header file search path (-I). + Returns a list of command-line options suitable for either + Unix compilers or Visual C++.""" + + # XXX it would be nice (mainly aesthetic, and so we don't generate + # stupid-looking command lines) to go over 'macros' and eliminate + # redundant definitions/undefinitions (ie. ensure that only the + # latest mention of a particular macro winds up on the command + # line). I don't think it's essential, though, since most (all?) + # Unix C compilers only pay attention to the latest -D or -U + # mention of a macro on their command line. Similar situation for + # 'include_dirs'. I'm punting on both for now. Anyways, weeding out + # redundancies like this should probably be the province of + # CCompiler, since the data structures used are inherited from it + # and therefore common to all CCompiler classes. + + pp_opts = [] + for macro in macros: + + if not (type (macro) is TupleType and + 1 <= len (macro) <= 2): + raise TypeError, \ + ("bad macro definition '%s': " + + "each element of 'macros' list must be a 1- or 2-tuple") % \ + macro + + if len (macro) == 1: # undefine this macro + pp_opts.append ("-U%s" % macro[0]) + elif len (macro) == 2: + if macro[1] is None: # define with no explicit value + pp_opts.append ("-D%s" % macro[0]) + else: + # XXX *don't* need to be clever about quoting the + # macro value here, because we're going to avoid the + # shell at all costs when we spawn the command! + pp_opts.append ("-D%s=%s" % macro) + + for dir in include_dirs: + pp_opts.append ("-I%s" % dir) + + return pp_opts + +# gen_preprocess_options () + + +def gen_lib_options (compiler, library_dirs, runtime_library_dirs, libraries): + """Generate linker options for searching library directories and + linking with specific libraries. 'libraries' and 'library_dirs' + are, respectively, lists of library names (not filenames!) and + search directories. Returns a list of command-line options suitable + for use with some compiler (depending on the two format strings + passed in).""" + + lib_opts = [] + + for dir in library_dirs: + lib_opts.append (compiler.library_dir_option (dir)) + + for dir in runtime_library_dirs: + lib_opts.append (compiler.runtime_library_dir_option (dir)) + + # XXX it's important that we *not* remove redundant library mentions! + # sometimes you really do have to say "-lfoo -lbar -lfoo" in order to + # resolve all symbols. I just hope we never have to say "-lfoo obj.o + # -lbar" to get things to work -- that's certainly a possibility, but a + # pretty nasty way to arrange your C code. + + for lib in libraries: + (lib_dir, lib_name) = os.path.split (lib) + if lib_dir: + lib_file = compiler.find_library_file ([lib_dir], lib_name) + if lib_file: + lib_opts.append (lib_file) + else: + compiler.warn ("no library file corresponding to " + "'%s' found (skipping)" % lib) + else: + lib_opts.append (compiler.library_option (lib)) + + return lib_opts + +# gen_lib_options () diff --git a/distutils/core.py b/distutils/core.py new file mode 100644 index 00000000..522d2f6b --- /dev/null +++ b/distutils/core.py @@ -0,0 +1,1016 @@ +"""distutils.core + +The only module that needs to be imported to use the Distutils; provides +the 'setup' function (which must be called); the 'Distribution' class +(which may be subclassed if additional functionality is desired), and +the 'Command' class (which is used both internally by Distutils, and +may be subclassed by clients for still more flexibility).""" + +# created 1999/03/01, Greg Ward + +__revision__ = "$Id$" + +import sys, os +import string, re +from types import * +from copy import copy +from distutils.errors import * +from distutils.fancy_getopt import fancy_getopt, print_help +from distutils import util + +# Regex to define acceptable Distutils command names. This is not *quite* +# the same as a Python NAME -- I don't allow leading underscores. The fact +# that they're very similar is no coincidence; the default naming scheme is +# to look for a Python module named after the command. +command_re = re.compile (r'^[a-zA-Z]([a-zA-Z0-9_]*)$') + +# This is a barebones help message generated displayed when the user +# runs the setup script with no arguments at all. More useful help +# is generated with various --help options: global help, list commands, +# and per-command help. +usage = """\ +usage: %s [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...] + or: %s --help + or: %s --help-commands + or: %s cmd --help +""" % ((sys.argv[0],) * 4) + + +def setup (**attrs): + """The gateway to the Distutils: do everything your setup script + needs to do, in a highly flexible and user-driven way. Briefly: + create a Distribution instance; parse the command-line, creating + and customizing instances of the command class for each command + found on the command-line; run each of those commands. + + The Distribution instance might be an instance of a class + supplied via the 'distclass' keyword argument to 'setup'; if no + such class is supplied, then the 'Distribution' class (also in + this module) is instantiated. All other arguments to 'setup' + (except for 'cmdclass') are used to set attributes of the + Distribution instance. + + The 'cmdclass' argument, if supplied, is a dictionary mapping + command names to command classes. Each command encountered on + the command line will be turned into a command class, which is in + turn instantiated; any class found in 'cmdclass' is used in place + of the default, which is (for command 'foo_bar') class 'foo_bar' + in module 'distutils.command.foo_bar'. The command class must + provide a 'user_options' attribute which is a list of option + specifiers for 'distutils.fancy_getopt'. Any command-line + options between the current and the next command are used to set + attributes of the current command object. + + When the entire command-line has been successfully parsed, calls + the 'run()' method on each command object in turn. This method + will be driven entirely by the Distribution object (which each + command object has a reference to, thanks to its constructor), + and the command-specific options that became attributes of each + command object.""" + + # Determine the distribution class -- either caller-supplied or + # our Distribution (see below). + klass = attrs.get ('distclass') + if klass: + del attrs['distclass'] + else: + klass = Distribution + + # Create the Distribution instance, using the remaining arguments + # (ie. everything except distclass) to initialize it + dist = klass (attrs) + + # If we had a config file, this is where we would parse it: override + # the client-supplied command options, but be overridden by the + # command line. + + # Parse the command line; any command-line errors are the end-users + # fault, so turn them into SystemExit to suppress tracebacks. + try: + ok = dist.parse_command_line (sys.argv[1:]) + except DistutilsArgError, msg: + sys.stderr.write (usage + "\n") + raise SystemExit, "error: %s" % msg + + # And finally, run all the commands found on the command line. + if ok: + try: + dist.run_commands () + except KeyboardInterrupt: + raise SystemExit, "interrupted" + except IOError, exc: + # arg, try to work with Python pre-1.5.2 + if hasattr (exc, 'filename') and hasattr (exc, 'strerror'): + raise SystemExit, \ + "error: %s: %s" % (exc.filename, exc.strerror) + else: + raise SystemExit, str (exc) + +# setup () + + +class Distribution: + """The core of the Distutils. Most of the work hiding behind + 'setup' is really done within a Distribution instance, which + farms the work out to the Distutils commands specified on the + command line. + + Clients will almost never instantiate Distribution directly, + unless the 'setup' function is totally inadequate to their needs. + However, it is conceivable that a client might wish to subclass + Distribution for some specialized purpose, and then pass the + subclass to 'setup' as the 'distclass' keyword argument. If so, + it is necessary to respect the expectations that 'setup' has of + Distribution: it must have a constructor and methods + 'parse_command_line()' and 'run_commands()' with signatures like + those described below.""" + + + # 'global_options' describes the command-line options that may be + # supplied to the client (setup.py) prior to any actual commands. + # Eg. "./setup.py -nv" or "./setup.py --verbose" both take advantage of + # these global options. This list should be kept to a bare minimum, + # since every global option is also valid as a command option -- and we + # don't want to pollute the commands with too many options that they + # have minimal control over. + global_options = [('verbose', 'v', + "run verbosely (default)"), + ('print-version', 'V', 'print package version'), + ('quiet', 'q', + "run quietly (turns verbosity off)"), + ('dry-run', 'n', + "don't actually do anything"), + ('force', 'f', + "skip dependency checking between files"), + ('help', 'h', + "show this help message"), + ] + negative_opt = {'quiet': 'verbose'} + + + # -- Creation/initialization methods ------------------------------- + + def __init__ (self, attrs=None): + """Construct a new Distribution instance: initialize all the + attributes of a Distribution, and then uses 'attrs' (a + dictionary mapping attribute names to values) to assign + some of those attributes their "real" values. (Any attributes + not mentioned in 'attrs' will be assigned to some null + value: 0, None, an empty list or dictionary, etc.) Most + importantly, initialize the 'command_obj' attribute + to the empty dictionary; this will be filled in with real + command objects by 'parse_command_line()'.""" + + # Default values for our command-line options + self.verbose = 1 + self.dry_run = 0 + self.force = 0 + self.help = 0 + self.help_commands = 0 + self.print_version = 0 + + # And the "distribution meta-data" options -- these can only + # come from setup.py (the caller), not the command line + # (or a hypothetical config file). + self.name = None + self.version = None + self.author = None + self.author_email = None + self.maintainer = None + self.maintainer_email = None + self.url = None + self.licence = None + self.description = None + self.outfiles = [] + self.scripts = None + self.programs = None + + # 'cmdclass' maps command names to class objects, so we + # can 1) quickly figure out which class to instantiate when + # we need to create a new command object, and 2) have a way + # for the client to override command classes + self.cmdclass = {} + + # These options are really the business of various commands, rather + # than of the Distribution itself. We provide aliases for them in + # Distribution as a convenience to the developer. + # dictionary. + self.packages = None + self.package_dir = None + self.py_modules = None + self.libraries = None + self.ext_modules = None + self.ext_package = None + self.include_dirs = None + self.install_path = None + + # And now initialize bookkeeping stuff that can't be supplied by + # the caller at all. 'command_obj' maps command names to + # Command instances -- that's how we enforce that every command + # class is a singleton. + self.command_obj = {} + + # 'have_run' maps command names to boolean values; it keeps track + # of whether we have actually run a particular command, to make it + # cheap to "run" a command whenever we think we might need to -- if + # it's already been done, no need for expensive filesystem + # operations, we just check the 'have_run' dictionary and carry on. + # It's only safe to query 'have_run' for a command class that has + # been instantiated -- a false value will be inserted when the + # command object is created, and replaced with a true value when + # the command is succesfully run. Thus it's probably best to use + # '.get()' rather than a straight lookup. + self.have_run = {} + + # Now we'll use the attrs dictionary (ultimately, keyword args from + # the client) to possibly override any or all of these distribution + # options. + if attrs: + + # Pull out the set of command options and work on them + # specifically. Note that this order guarantees that aliased + # command options will override any supplied redundantly + # through the general options dictionary. + options = attrs.get ('options') + if options: + del attrs['options'] + for (command, cmd_options) in options.items(): + cmd_obj = self.find_command_obj (command) + for (key, val) in cmd_options.items(): + cmd_obj.set_option (key, val) + # loop over commands + # if any command options + + # Now work on the rest of the attributes. Any attribute that's + # not already defined is invalid! + for (key,val) in attrs.items(): + if hasattr (self, key): + setattr (self, key, val) + else: + raise DistutilsOptionError, \ + "invalid distribution option '%s'" % key + + # __init__ () + + + def parse_command_line (self, args): + """Parse the setup script's command line: set any Distribution + attributes tied to command-line options, create all command + objects, and set their options from the command-line. 'args' + must be a list of command-line arguments, most likely + 'sys.argv[1:]' (see the 'setup()' function). This list is first + processed for "global options" -- options that set attributes of + the Distribution instance. Then, it is alternately scanned for + Distutils command and options for that command. Each new + command terminates the options for the previous command. The + allowed options for a command are determined by the 'options' + attribute of the command object -- thus, we instantiate (and + cache) every command object here, in order to access its + 'options' attribute. Any error in that 'options' attribute + raises DistutilsGetoptError; any error on the command-line + raises DistutilsArgError. If no Distutils commands were found + on the command line, raises DistutilsArgError. Return true if + command-line successfully parsed and we should carry on with + executing commands; false if no errors but we shouldn't execute + commands (currently, this only happens if user asks for + help).""" + + # We have to parse the command line a bit at a time -- global + # options, then the first command, then its options, and so on -- + # because each command will be handled by a different class, and + # the options that are valid for a particular class aren't + # known until we instantiate the command class, which doesn't + # happen until we know what the command is. + + self.commands = [] + options = self.global_options + \ + [('help-commands', None, + "list all available commands")] + args = fancy_getopt (options, self.negative_opt, + self, sys.argv[1:]) + + # User just wants a list of commands -- we'll print it out and stop + # processing now (ie. if they ran "setup --help-commands foo bar", + # we ignore "foo bar"). + if self.help_commands: + self.print_commands () + print + print usage + return + if self.print_version: + print self.version + return + + while args: + # Pull the current command from the head of the command line + command = args[0] + if not command_re.match (command): + raise SystemExit, "invalid command name '%s'" % command + self.commands.append (command) + + # Make sure we have a command object to put the options into + # (this either pulls it out of a cache of command objects, + # or finds and instantiates the command class). + try: + cmd_obj = self.find_command_obj (command) + except DistutilsModuleError, msg: + raise DistutilsArgError, msg + + # Require that the command class be derived from Command -- + # that way, we can be sure that we at least have the 'run' + # and 'get_option' methods. + if not isinstance (cmd_obj, Command): + raise DistutilsClassError, \ + "command class %s must subclass Command" % \ + cmd_obj.__class__ + + # Also make sure that the command object provides a list of its + # known options + if not (hasattr (cmd_obj, 'user_options') and + type (cmd_obj.user_options) is ListType): + raise DistutilsClassError, \ + ("command class %s must provide " + + "'user_options' attribute (a list of tuples)") % \ + cmd_obj.__class__ + + # Poof! like magic, all commands support the global + # options too, just by adding in 'global_options'. + negative_opt = self.negative_opt + if hasattr (cmd_obj, 'negative_opt'): + negative_opt = copy (negative_opt) + negative_opt.update (cmd_obj.negative_opt) + + options = self.global_options + cmd_obj.user_options + args = fancy_getopt (options, negative_opt, + cmd_obj, args[1:]) + if cmd_obj.help: + print_help (self.global_options, + header="Global options:") + print + print_help (cmd_obj.user_options, + header="Options for '%s' command:" % command) + print + print usage + return + + self.command_obj[command] = cmd_obj + self.have_run[command] = 0 + + # while args + + # If the user wants help -- ie. they gave the "--help" option -- + # give it to 'em. We do this *after* processing the commands in + # case they want help on any particular command, eg. + # "setup.py --help foo". (This isn't the documented way to + # get help on a command, but I support it because that's how + # CVS does it -- might as well be consistent.) + if self.help: + print_help (self.global_options, header="Global options:") + print + + for command in self.commands: + klass = self.find_command_class (command) + print_help (klass.user_options, + header="Options for '%s' command:" % command) + print + + print usage + return + + # Oops, no commands found -- an end-user error + if not self.commands: + raise DistutilsArgError, "no commands supplied" + + # All is well: return true + return 1 + + # parse_command_line() + + + def print_command_list (self, commands, header, max_length): + """Print a subset of the list of all commands -- used by + 'print_commands()'.""" + + print header + ":" + + for cmd in commands: + klass = self.cmdclass.get (cmd) + if not klass: + klass = self.find_command_class (cmd) + try: + description = klass.description + except AttributeError: + description = "(no description available)" + + print " %-*s %s" % (max_length, cmd, description) + + # print_command_list () + + + def print_commands (self): + """Print out a help message listing all available commands with + a description of each. The list is divided into "standard + commands" (listed in distutils.command.__all__) and "extra + commands" (mentioned in self.cmdclass, but not a standard + command). The descriptions come from the command class + attribute 'description'.""" + + import distutils.command + std_commands = distutils.command.__all__ + is_std = {} + for cmd in std_commands: + is_std[cmd] = 1 + + extra_commands = [] + for cmd in self.cmdclass.keys(): + if not is_std.get(cmd): + extra_commands.append (cmd) + + max_length = 0 + for cmd in (std_commands + extra_commands): + if len (cmd) > max_length: + max_length = len (cmd) + + self.print_command_list (std_commands, + "Standard commands", + max_length) + if extra_commands: + print + self.print_command_list (extra_commands, + "Extra commands", + max_length) + + # print_commands () + + + + # -- Command class/object methods ---------------------------------- + + # This is a method just so it can be overridden if desired; it doesn't + # actually use or change any attributes of the Distribution instance. + def find_command_class (self, command): + """Given a command, derives the names of the module and class + expected to implement the command: eg. 'foo_bar' becomes + 'distutils.command.foo_bar' (the module) and 'FooBar' (the + class within that module). Loads the module, extracts the + class from it, and returns the class object. + + Raises DistutilsModuleError with a semi-user-targeted error + message if the expected module could not be loaded, or the + expected class was not found in it.""" + + module_name = 'distutils.command.' + command + klass_name = command + + try: + __import__ (module_name) + module = sys.modules[module_name] + except ImportError: + raise DistutilsModuleError, \ + "invalid command '%s' (no module named '%s')" % \ + (command, module_name) + + try: + klass = vars(module)[klass_name] + except KeyError: + raise DistutilsModuleError, \ + "invalid command '%s' (no class '%s' in module '%s')" \ + % (command, klass_name, module_name) + + return klass + + # find_command_class () + + + def create_command_obj (self, command): + """Figure out the class that should implement a command, + instantiate it, cache and return the new "command object". + The "command class" is determined either by looking it up in + the 'cmdclass' attribute (this is the mechanism whereby + clients may override default Distutils commands or add their + own), or by calling the 'find_command_class()' method (if the + command name is not in 'cmdclass'.""" + + # Determine the command class -- either it's in the command_class + # dictionary, or we have to divine the module and class name + klass = self.cmdclass.get(command) + if not klass: + klass = self.find_command_class (command) + self.cmdclass[command] = klass + + # Found the class OK -- instantiate it + cmd_obj = klass (self) + return cmd_obj + + + def find_command_obj (self, command, create=1): + """Look up and return a command object in the cache maintained by + 'create_command_obj()'. If none found, the action taken + depends on 'create': if true (the default), create a new + command object by calling 'create_command_obj()' and return + it; otherwise, return None. If 'command' is an invalid + command name, then DistutilsModuleError will be raised.""" + + cmd_obj = self.command_obj.get (command) + if not cmd_obj and create: + cmd_obj = self.create_command_obj (command) + self.command_obj[command] = cmd_obj + + return cmd_obj + + + # -- Methods that operate on the Distribution ---------------------- + + def announce (self, msg, level=1): + """Print 'msg' if 'level' is greater than or equal to the verbosity + level recorded in the 'verbose' attribute (which, currently, + can be only 0 or 1).""" + + if self.verbose >= level: + print msg + + + def run_commands (self): + """Run each command that was seen on the client command line. + Uses the list of commands found and cache of command objects + created by 'create_command_obj()'.""" + + for cmd in self.commands: + self.run_command (cmd) + + + def get_option (self, option): + """Return the value of a distribution option. Raise + DistutilsOptionError if 'option' is not known.""" + + try: + return getattr (self, opt) + except AttributeError: + raise DistutilsOptionError, \ + "unknown distribution option %s" % option + + + def get_options (self, *options): + """Return (as a tuple) the values of several distribution + options. Raise DistutilsOptionError if any element of + 'options' is not known.""" + + values = [] + try: + for opt in options: + values.append (getattr (self, opt)) + except AttributeError, name: + raise DistutilsOptionError, \ + "unknown distribution option %s" % name + + return tuple (values) + + + # -- Methods that operate on its Commands -------------------------- + + def run_command (self, command): + + """Do whatever it takes to run a command (including nothing at all, + if the command has already been run). Specifically: if we have + already created and run the command named by 'command', return + silently without doing anything. If the command named by + 'command' doesn't even have a command object yet, create one. + Then invoke 'run()' on that command object (or an existing + one).""" + + # Already been here, done that? then return silently. + if self.have_run.get (command): + return + + self.announce ("running " + command) + cmd_obj = self.find_command_obj (command) + cmd_obj.ensure_ready () + cmd_obj.run () + self.have_run[command] = 1 + + + def get_command_option (self, command, option): + """Create a command object for 'command' if necessary, ensure that + its option values are all set to their final values, and return + the value of its 'option' option. Raise DistutilsOptionError if + 'option' is not known for that 'command'.""" + + cmd_obj = self.find_command_obj (command) + cmd_obj.ensure_ready () + return cmd_obj.get_option (option) + try: + return getattr (cmd_obj, option) + except AttributeError: + raise DistutilsOptionError, \ + "command %s: no such option %s" % (command, option) + + + def get_command_options (self, command, *options): + """Create a command object for 'command' if necessary, ensure that + its option values are all set to their final values, and return + a tuple containing the values of all the options listed in + 'options' for that command. Raise DistutilsOptionError if any + invalid option is supplied in 'options'.""" + + cmd_obj = self.find_command_obj (command) + cmd_obj.ensure_ready () + values = [] + try: + for opt in options: + values.append (getattr (cmd_obj, option)) + except AttributeError, name: + raise DistutilsOptionError, \ + "command %s: no such option %s" % (command, name) + + return tuple (values) + +# end class Distribution + + +class Command: + """Abstract base class for defining command classes, the "worker bees" + of the Distutils. A useful analogy for command classes is to + think of them as subroutines with local variables called + "options". The options are "declared" in 'initialize_options()' + and "defined" (given their final values, aka "finalized") in + 'finalize_options()', both of which must be defined by every + command class. The distinction between the two is necessary + because option values might come from the outside world (command + line, option file, ...), and any options dependent on other + options must be computed *after* these outside influences have + been processed -- hence 'finalize_options()'. The "body" of the + subroutine, where it does all its work based on the values of its + options, is the 'run()' method, which must also be implemented by + every command class.""" + + # -- Creation/initialization methods ------------------------------- + + def __init__ (self, dist): + """Create and initialize a new Command object. Most importantly, + invokes the 'initialize_options()' method, which is the + real initializer and depends on the actual command being + instantiated.""" + + if not isinstance (dist, Distribution): + raise TypeError, "dist must be a Distribution instance" + if self.__class__ is Command: + raise RuntimeError, "Command is an abstract class" + + self.distribution = dist + self.initialize_options () + + # Per-command versions of the global flags, so that the user can + # customize Distutils' behaviour command-by-command and let some + # commands fallback on the Distribution's behaviour. None means + # "not defined, check self.distribution's copy", while 0 or 1 mean + # false and true (duh). Note that this means figuring out the real + # value of each flag is a touch complicatd -- hence "self.verbose" + # (etc.) will be handled by __getattr__, below. + self._verbose = None + self._dry_run = None + self._force = None + + # The 'help' flag is just used for command-line parsing, so + # none of that complicated bureaucracy is needed. + self.help = 0 + + # 'ready' records whether or not 'finalize_options()' has been + # called. 'finalize_options()' itself should not pay attention to + # this flag: it is the business of 'ensure_ready()', which always + # calls 'finalize_options()', to respect/update it. + self.ready = 0 + + # end __init__ () + + + def __getattr__ (self, attr): + if attr in ('verbose', 'dry_run', 'force'): + myval = getattr (self, "_" + attr) + if myval is None: + return getattr (self.distribution, attr) + else: + return myval + else: + raise AttributeError, attr + + + def ensure_ready (self): + if not self.ready: + self.finalize_options () + self.ready = 1 + + + # Subclasses must define: + # initialize_options() + # provide default values for all options; may be overridden + # by Distutils client, by command-line options, or by options + # from option file + # finalize_options() + # decide on the final values for all options; this is called + # after all possible intervention from the outside world + # (command-line, option file, etc.) has been processed + # run() + # run the command: do whatever it is we're here to do, + # controlled by the command's various option values + + def initialize_options (self): + """Set default values for all the options that this command + supports. Note that these defaults may be overridden + by the command-line supplied by the user; thus, this is + not the place to code dependencies between options; generally, + 'initialize_options()' implementations are just a bunch + of "self.foo = None" assignments. + + This method must be implemented by all command classes.""" + + raise RuntimeError, \ + "abstract method -- subclass %s must override" % self.__class__ + + def finalize_options (self): + """Set final values for all the options that this command + supports. This is always called as late as possible, ie. + after any option assignments from the command-line or from + other commands have been done. Thus, this is the place to to + code option dependencies: if 'foo' depends on 'bar', then it + is safe to set 'foo' from 'bar' as long as 'foo' still has + the same value it was assigned in 'initialize_options()'. + + This method must be implemented by all command classes.""" + + raise RuntimeError, \ + "abstract method -- subclass %s must override" % self.__class__ + + def run (self): + """A command's raison d'etre: carry out the action it exists + to perform, controlled by the options initialized in + 'initialize_options()', customized by the user and other + commands, and finalized in 'finalize_options()'. All + terminal output and filesystem interaction should be done by + 'run()'. + + This method must be implemented by all command classes.""" + + raise RuntimeError, \ + "abstract method -- subclass %s must override" % self.__class__ + + def announce (self, msg, level=1): + """If the Distribution instance to which this command belongs + has a verbosity level of greater than or equal to 'level' + print 'msg' to stdout.""" + + if self.verbose >= level: + print msg + + + # -- Option query/set methods -------------------------------------- + + def get_option (self, option): + """Return the value of a single option for this command. Raise + DistutilsOptionError if 'option' is not known.""" + try: + return getattr (self, option) + except AttributeError: + raise DistutilsOptionError, \ + "command %s: no such option %s" % \ + (self.get_command_name(), option) + + + def get_options (self, *options): + """Return (as a tuple) the values of several options for this + command. Raise DistutilsOptionError if any of the options in + 'options' are not known.""" + + values = [] + try: + for opt in options: + values.append (getattr (self, opt)) + except AttributeError, name: + raise DistutilsOptionError, \ + "command %s: no such option %s" % \ + (self.get_command_name(), name) + + return tuple (values) + + + def set_option (self, option, value): + """Set the value of a single option for this command. Raise + DistutilsOptionError if 'option' is not known.""" + + if not hasattr (self, option): + raise DistutilsOptionError, \ + "command '%s': no such option '%s'" % \ + (self.get_command_name(), option) + if value is not None: + setattr (self, option, value) + + def set_options (self, **optval): + """Set the values of several options for this command. Raise + DistutilsOptionError if any of the options specified as + keyword arguments are not known.""" + + for k in optval.keys(): + if optval[k] is not None: + self.set_option (k, optval[k]) + + + # -- Convenience methods for commands ------------------------------ + + def get_command_name (self): + if hasattr (self, 'command_name'): + return self.command_name + else: + class_name = self.__class__.__name__ + + # The re.split here returs empty strings delimited by the + # words we're actually interested in -- e.g. "FooBarBaz" + # splits to ['', 'Foo', '', 'Bar', '', 'Baz', '']. Hence + # the 'filter' to strip out the empties. + words = filter (None, re.split (r'([A-Z][a-z]+)', class_name)) + self.command_name = string.join (map (string.lower, words), "_") + return self.command_name + + + def set_undefined_options (self, src_cmd, *option_pairs): + """Set the values of any "undefined" options from corresponding + option values in some other command object. "Undefined" here + means "is None", which is the convention used to indicate + that an option has not been changed between + 'set_initial_values()' and 'set_final_values()'. Usually + called from 'set_final_values()' for options that depend on + some other command rather than another option of the same + command. 'src_cmd' is the other command from which option + values will be taken (a command object will be created for it + if necessary); the remaining arguments are + '(src_option,dst_option)' tuples which mean "take the value + of 'src_option' in the 'src_cmd' command object, and copy it + to 'dst_option' in the current command object".""" + + # Option_pairs: list of (src_option, dst_option) tuples + + src_cmd_obj = self.distribution.find_command_obj (src_cmd) + src_cmd_obj.finalize_options () + try: + for (src_option, dst_option) in option_pairs: + if getattr (self, dst_option) is None: + self.set_option (dst_option, + src_cmd_obj.get_option (src_option)) + except AttributeError, name: + # duh, which command? + raise DistutilsOptionError, "unknown option %s" % name + + + def set_peer_option (self, command, option, value): + """Attempt to simulate a command-line override of some option + value in another command. Finds the command object for + 'command', sets its 'option' to 'value', and unconditionally + calls 'finalize_options()' on it: this means that some command + objects may have 'finalize_options()' invoked more than once. + Even so, this is not entirely reliable: the other command may + already be initialized to its satisfaction, in which case the + second 'finalize_options()' invocation will have little or no + effect.""" + + cmd_obj = self.distribution.find_command_obj (command) + cmd_obj.set_option (option, value) + cmd_obj.finalize_options () + + + def find_peer (self, command, create=1): + """Wrapper around Distribution's 'find_command_obj()' method: + find (create if necessary and 'create' is true) the command + object for 'command'..""" + + cmd_obj = self.distribution.find_command_obj (command, create) + cmd_obj.ensure_ready () + return cmd_obj + + + def get_peer_option (self, command, option): + """Find or create the command object for 'command', and return + its 'option' option.""" + + cmd_obj = self.distribution.find_command_obj (command) + return cmd_obj.get_option (option) + + + def run_peer (self, command): + """Run some other command: uses the 'run_command()' method of + Distribution, which creates the command object if necessary + and then invokes its 'run()' method.""" + + self.distribution.run_command (command) + + + # -- External world manipulation ----------------------------------- + + def warn (self, msg): + sys.stderr.write ("warning: %s: %s\n" % + (self.get_command_name(), msg)) + + + def execute (self, func, args, msg=None, level=1): + """Perform some action that affects the outside world (eg. + by writing to the filesystem). Such actions are special because + they should be disabled by the "dry run" flag, and should + announce themselves if the current verbosity level is high + enough. This method takes care of all that bureaucracy for you; + all you have to do is supply the funtion to call and an argument + tuple for it (to embody the "external action" being performed), + a message to print if the verbosity level is high enough, and an + optional verbosity threshold.""" + + # Generate a message if we weren't passed one + if msg is None: + msg = "%s %s" % (func.__name__, `args`) + if msg[-2:] == ',)': # correct for singleton tuple + msg = msg[0:-2] + ')' + + # Print it if verbosity level is high enough + self.announce (msg, level) + + # And do it, as long as we're not in dry-run mode + if not self.dry_run: + apply (func, args) + + # execute() + + + def mkpath (self, name, mode=0777): + return util.mkpath (name, mode, self.verbose, self.dry_run) + + + def copy_file (self, infile, outfile, + preserve_mode=1, preserve_times=1, level=1): + """Copy a file respecting verbose, dry-run and force flags.""" + + return util.copy_file (infile, outfile, + preserve_mode, preserve_times, + not self.force, + self.verbose >= level, + self.dry_run) + + + def copy_tree (self, infile, outfile, + preserve_mode=1, preserve_times=1, preserve_symlinks=0, + level=1): + """Copy an entire directory tree respecting verbose, dry-run, + and force flags.""" + + return util.copy_tree (infile, outfile, + preserve_mode,preserve_times,preserve_symlinks, + not self.force, + self.verbose >= level, + self.dry_run) + + + def move_file (self, src, dst, level=1): + """Move a file respecting verbose and dry-run flags.""" + return util.move_file (src, dst, + self.verbose >= level, + self.dry_run) + + + def spawn (self, cmd, search_path=1, level=1): + from distutils.spawn import spawn + spawn (cmd, search_path, + self.verbose >= level, + self.dry_run) + + + def make_file (self, infiles, outfile, func, args, + exec_msg=None, skip_msg=None, level=1): + + """Special case of 'execute()' for operations that process one or + more input files and generate one output file. Works just like + 'execute()', except the operation is skipped and a different + message printed if 'outfile' already exists and is newer than + all files listed in 'infiles'.""" + + + if exec_msg is None: + exec_msg = "generating %s from %s" % \ + (outfile, string.join (infiles, ', ')) + if skip_msg is None: + skip_msg = "skipping %s (inputs unchanged)" % outfile + + + # Allow 'infiles' to be a single string + if type (infiles) is StringType: + infiles = (infiles,) + elif type (infiles) not in (ListType, TupleType): + raise TypeError, \ + "'infiles' must be a string, or a list or tuple of strings" + + # If 'outfile' must be regenerated (either because it doesn't + # exist, is out-of-date, or the 'force' flag is true) then + # perform the action that presumably regenerates it + if self.force or util.newer_group (infiles, outfile): + self.execute (func, args, exec_msg, level) + + # Otherwise, print the "skip" message + else: + self.announce (skip_msg, level) + + # make_file () + +# end class Command diff --git a/distutils/errors.py b/distutils/errors.py new file mode 100644 index 00000000..86d91dd6 --- /dev/null +++ b/distutils/errors.py @@ -0,0 +1,84 @@ +"""distutils.errors + +Provides exceptions used by the Distutils modules. Note that Distutils +modules may raise standard exceptions; in particular, SystemExit is +usually raised for errors that are obviously the end-user's fault +(eg. bad command-line arguments). + +This module safe to use in "from ... import *" mode; it only exports +symbols whose names start with "Distutils" and end with "Error".""" + +# created 1999/03/03, Greg Ward + +__revision__ = "$Id$" + +import types + +if type (RuntimeError) is types.ClassType: + + # DistutilsError is the root of all Distutils evil. + class DistutilsError (Exception): + pass + + # DistutilsModuleError is raised if we are unable to load an expected + # module, or find an expected class within some module + class DistutilsModuleError (DistutilsError): + pass + + # DistutilsClassError is raised if we encounter a distribution or command + # class that's not holding up its end of the bargain. + class DistutilsClassError (DistutilsError): + pass + + # DistutilsGetoptError (help me -- I have JavaProgrammersDisease!) is + # raised if the option table provided to fancy_getopt is bogus. + class DistutilsGetoptError (DistutilsError): + pass + + # DistutilsArgError is raised by fancy_getopt in response to getopt.error; + # distutils.core then turns around and raises SystemExit from that. (Thus + # client code should never see DistutilsArgError.) + class DistutilsArgError (DistutilsError): + pass + + # DistutilsFileError is raised for any problems in the filesystem: + # expected file not found, etc. + class DistutilsFileError (DistutilsError): + pass + + # DistutilsOptionError is raised anytime an attempt is made to access + # (get or set) an option that does not exist for a particular command + # (or for the distribution itself). + class DistutilsOptionError (DistutilsError): + pass + + # DistutilsValueError is raised anytime an option value (presumably + # provided by setup.py) is invalid. + class DistutilsValueError (DistutilsError): + pass + + # DistutilsPlatformError is raised when we find that we don't + # know how to do something on the current platform (but we do + # know how to do it on some platform). + class DistutilsPlatformError (DistutilsError): + pass + + # DistutilsExecError is raised if there are any problems executing + # an external program + class DistutilsExecError (DistutilsError): + pass + +# String-based exceptions +else: + DistutilsError = 'DistutilsError' + DistutilsModuleError = 'DistutilsModuleError' + DistutilsClassError = 'DistutilsClassError' + DistutilsGetoptError = 'DistutilsGetoptError' + DistutilsArgError = 'DistutilsArgError' + DistutilsFileError = 'DistutilsFileError' + DistutilsOptionError = 'DistutilsOptionError' + DistutilsValueError = 'DistutilsValueError' + DistutilsPlatformError = 'DistutilsPlatformError' + DistutilsExecError = 'DistutilsExecError' + +del types diff --git a/distutils/fancy_getopt.py b/distutils/fancy_getopt.py new file mode 100644 index 00000000..3110ab30 --- /dev/null +++ b/distutils/fancy_getopt.py @@ -0,0 +1,311 @@ +"""distutils.fancy_getopt + +Wrapper around the standard getopt module that provides the following +additional features: + * short and long options are tied together + * options have help strings, so fancy_getopt could potentially + create a complete usage summary + * options set attributes of a passed-in object +""" + +# created 1999/03/03, Greg Ward + +__revision__ = "$Id$" + +import sys, string, re +from types import * +import getopt +from distutils.errors import * + +# Much like command_re in distutils.core, this is close to but not quite +# the same as a Python NAME -- except, in the spirit of most GNU +# utilities, we use '-' in place of '_'. (The spirit of LISP lives on!) +# The similarities to NAME are again not a coincidence... +longopt_pat = r'[a-zA-Z](?:[a-zA-Z0-9-]*)' +longopt_re = re.compile (r'^%s$' % longopt_pat) + +# For recognizing "negative alias" options, eg. "quiet=!verbose" +neg_alias_re = re.compile ("^(%s)=!(%s)$" % (longopt_pat, longopt_pat)) + + +# This is used to translate long options to legitimate Python identifiers +# (for use as attributes of some object). +longopt_xlate = string.maketrans ('-', '_') + + +def fancy_getopt (options, negative_opt, object, args): + + # The 'options' table is a list of 3-tuples: + # (long_option, short_option, help_string) + # if an option takes an argument, its long_option should have '=' + # appended; short_option should just be a single character, no ':' in + # any case. If a long_option doesn't have a corresponding + # short_option, short_option should be None. All option tuples must + # have long options. + + # Build the short_opts string and long_opts list, remembering how + # the two are tied together + + short_opts = [] # we'll join 'em when done + long_opts = [] + short2long = {} + attr_name = {} + takes_arg = {} + + for option in options: + try: + (long, short, help) = option + except ValueError: + raise DistutilsGetoptError, \ + "invalid option tuple " + str (option) + + # Type-check the option names + if type (long) is not StringType or len (long) < 2: + raise DistutilsGetoptError, \ + "long option '%s' must be a string of length >= 2" % \ + long + + if (not ((short is None) or + (type (short) is StringType and len (short) == 1))): + raise DistutilsGetoptError, \ + "short option '%s' must be None or string of length 1" % \ + short + + long_opts.append (long) + + if long[-1] == '=': # option takes an argument? + if short: short = short + ':' + long = long[0:-1] + takes_arg[long] = 1 + else: + + # Is option is a "negative alias" for some other option (eg. + # "quiet" == "!verbose")? + alias_to = negative_opt.get(long) + if alias_to is not None: + if not takes_arg.has_key(alias_to) or takes_arg[alias_to]: + raise DistutilsGetoptError, \ + ("option '%s' is a negative alias for '%s', " + + "which either hasn't been defined yet " + + "or takes an argument") % (long, alias_to) + + long_opts[-1] = long + takes_arg[long] = 0 + + else: + takes_arg[long] = 0 + + + # Now enforce some bondage on the long option name, so we can later + # translate it to an attribute name in 'object'. Have to do this a + # bit late to make sure we've removed any trailing '='. + if not longopt_re.match (long): + raise DistutilsGetoptError, \ + ("invalid long option name '%s' " + + "(must be letters, numbers, hyphens only") % long + + attr_name[long] = string.translate (long, longopt_xlate) + if short: + short_opts.append (short) + short2long[short[0]] = long + + # end loop over 'options' + + short_opts = string.join (short_opts) + try: + (opts, args) = getopt.getopt (args, short_opts, long_opts) + except getopt.error, msg: + raise DistutilsArgError, msg + + for (opt, val) in opts: + if len (opt) == 2 and opt[0] == '-': # it's a short option + opt = short2long[opt[1]] + + elif len (opt) > 2 and opt[0:2] == '--': + opt = opt[2:] + + else: + raise RuntimeError, "getopt lies! (bad option string '%s')" % \ + opt + + attr = attr_name[opt] + if takes_arg[opt]: + setattr (object, attr, val) + else: + if val == '': + alias = negative_opt.get (opt) + if alias: + setattr (object, attr_name[alias], 0) + else: + setattr (object, attr, 1) + else: + raise RuntimeError, "getopt lies! (bad value '%s')" % value + + # end loop over options found in 'args' + + return args + +# fancy_getopt() + + +WS_TRANS = string.maketrans (string.whitespace, ' ' * len (string.whitespace)) + +def wrap_text (text, width): + + if text is None: + return [] + if len (text) <= width: + return [text] + + text = string.expandtabs (text) + text = string.translate (text, WS_TRANS) + chunks = re.split (r'( +|-+)', text) + chunks = filter (None, chunks) # ' - ' results in empty strings + lines = [] + + while chunks: + + cur_line = [] # list of chunks (to-be-joined) + cur_len = 0 # length of current line + + while chunks: + l = len (chunks[0]) + if cur_len + l <= width: # can squeeze (at least) this chunk in + cur_line.append (chunks[0]) + del chunks[0] + cur_len = cur_len + l + else: # this line is full + # drop last chunk if all space + if cur_line and cur_line[-1][0] == ' ': + del cur_line[-1] + break + + if chunks: # any chunks left to process? + + # if the current line is still empty, then we had a single + # chunk that's too big too fit on a line -- so we break + # down and break it up at the line width + if cur_len == 0: + cur_line.append (chunks[0][0:width]) + chunks[0] = chunks[0][width:] + + # all-whitespace chunks at the end of a line can be discarded + # (and we know from the re.split above that if a chunk has + # *any* whitespace, it is *all* whitespace) + if chunks[0][0] == ' ': + del chunks[0] + + # and store this line in the list-of-all-lines -- as a single + # string, of course! + lines.append (string.join (cur_line, '')) + + # while chunks + + return lines + +# wrap_text () + + +def generate_help (options, header=None): + """Generate help text (a list of strings, one per suggested line of + output) from an option table.""" + + # Blithely assume the option table is good: probably wouldn't call + # 'generate_help()' unless you've already called 'fancy_getopt()'. + + # First pass: determine maximum length of long option names + max_opt = 0 + for option in options: + long = option[0] + short = option[1] + l = len (long) + if long[-1] == '=': + l = l - 1 + if short is not None: + l = l + 5 # " (-x)" where short == 'x' + if l > max_opt: + max_opt = l + + opt_width = max_opt + 2 + 2 + 2 # room for indent + dashes + gutter + + # Typical help block looks like this: + # --foo controls foonabulation + # Help block for longest option looks like this: + # --flimflam set the flim-flam level + # and with wrapped text: + # --flimflam set the flim-flam level (must be between + # 0 and 100, except on Tuesdays) + # Options with short names will have the short name shown (but + # it doesn't contribute to max_opt): + # --foo (-f) controls foonabulation + # If adding the short option would make the left column too wide, + # we push the explanation off to the next line + # --flimflam (-l) + # set the flim-flam level + # Important parameters: + # - 2 spaces before option block start lines + # - 2 dashes for each long option name + # - min. 2 spaces between option and explanation (gutter) + # - 5 characters (incl. space) for short option name + + # Now generate lines of help text. + line_width = 78 # if 80 columns were good enough for + text_width = line_width - opt_width # Jesus, then 78 are good enough for me + big_indent = ' ' * opt_width + if header: + lines = [header] + else: + lines = ['Option summary:'] + + for (long,short,help) in options: + + text = wrap_text (help, text_width) + if long[-1] == '=': + long = long[0:-1] + + # Case 1: no short option at all (makes life easy) + if short is None: + if text: + lines.append (" --%-*s %s" % (max_opt, long, text[0])) + else: + lines.append (" --%-*s " % (max_opt, long)) + + for l in text[1:]: + lines.append (big_indent + l) + + # Case 2: we have a short option, so we have to include it + # just after the long option + else: + opt_names = "%s (-%s)" % (long, short) + if text: + lines.append (" --%-*s %s" % + (max_opt, opt_names, text[0])) + else: + lines.append (" --%-*s" % opt_names) + + # for loop over options + + return lines + +# generate_help () + + +def print_help (options, file=None, header=None): + if file is None: + file = sys.stdout + for line in generate_help (options, header): + file.write (line + "\n") +# print_help () + + +if __name__ == "__main__": + text = """\ +Tra-la-la, supercalifragilisticexpialidocious. +How *do* you spell that odd word, anyways? +(Someone ask Mary -- she'll know [or she'll +say, "How should I know?"].)""" + + for w in (10, 20, 30, 40): + print "width: %d" % w + print string.join (wrap_text (text, w), "\n") + print diff --git a/distutils/msvccompiler.py b/distutils/msvccompiler.py new file mode 100644 index 00000000..7324b8e1 --- /dev/null +++ b/distutils/msvccompiler.py @@ -0,0 +1,374 @@ +"""distutils.ccompiler + +Contains MSVCCompiler, an implementation of the abstract CCompiler class +for the Microsoft Visual Studio.""" + + +# created 1999/08/19, Perry Stoll +# hacked by Robin Becker and Thomas Heller to do a better job of +# finding DevStudio (through the registry) + +__revision__ = "$Id$" + +import sys, os, string +from types import * +from distutils.errors import * +from distutils.ccompiler import \ + CCompiler, gen_preprocess_options, gen_lib_options + + +def get_devstudio_versions (): + """Get list of devstudio versions from the Windows registry. Return a + list of strings containing version numbers; the list will be + empty if we were unable to access the registry (eg. couldn't import + a registry-access module) or the appropriate registry keys weren't + found.""" + + try: + import win32api + import win32con + except ImportError: + return [] + + K = 'Software\\Microsoft\\Devstudio' + L = [] + for base in (win32con.HKEY_CLASSES_ROOT, + win32con.HKEY_LOCAL_MACHINE, + win32con.HKEY_CURRENT_USER, + win32con.HKEY_USERS): + try: + k = win32api.RegOpenKeyEx(base,K) + i = 0 + while 1: + try: + p = win32api.RegEnumKey(k,i) + if p[0] in '123456789' and p not in L: + L.append(p) + except win32api.error: + break + i = i + 1 + except win32api.error: + pass + L.sort() + L.reverse() + return L + +# get_devstudio_versions () + + +def get_msvc_paths (path, version='6.0', platform='x86'): + """Get a list of devstudio directories (include, lib or path). Return + a list of strings; will be empty list if unable to access the + registry or appropriate registry keys not found.""" + + try: + import win32api + import win32con + except ImportError: + return [] + + L = [] + if path=='lib': + path= 'Library' + path = string.upper(path + ' Dirs') + K = ('Software\\Microsoft\\Devstudio\\%s\\' + + 'Build System\\Components\\Platforms\\Win32 (%s)\\Directories') % \ + (version,platform) + for base in (win32con.HKEY_CLASSES_ROOT, + win32con.HKEY_LOCAL_MACHINE, + win32con.HKEY_CURRENT_USER, + win32con.HKEY_USERS): + try: + k = win32api.RegOpenKeyEx(base,K) + i = 0 + while 1: + try: + (p,v,t) = win32api.RegEnumValue(k,i) + if string.upper(p) == path: + V = string.split(v,';') + for v in V: + if v == '' or v in L: continue + L.append(v) + break + i = i + 1 + except win32api.error: + break + except win32api.error: + pass + return L + +# get_msvc_paths() + + +def find_exe (exe, version_number): + """Try to find an MSVC executable program 'exe' (from version + 'version_number' of MSVC) in several places: first, one of the MSVC + program search paths from the registry; next, the directories in the + PATH environment variable. If any of those work, return an absolute + path that is known to exist. If none of them work, just return the + original program name, 'exe'.""" + + for p in get_msvc_paths ('path', version_number): + fn = os.path.join (os.path.abspath(p), exe) + if os.path.isfile(fn): + return fn + + # didn't find it; try existing path + for p in string.split (os.environ['Path'],';'): + fn = os.path.join(os.path.abspath(p),exe) + if os.path.isfile(fn): + return fn + + return exe # last desperate hope + + +def set_path_env_var (name, version_number): + """Set environment variable 'name' to an MSVC path type value obtained + from 'get_msvc_paths()'. This is equivalent to a SET command prior + to execution of spawned commands.""" + + p = get_msvc_paths (name, version_number) + if p: + os.environ[name] = string.join (p,';') + + +class MSVCCompiler (CCompiler) : + """Concrete class that implements an interface to Microsoft Visual C++, + as defined by the CCompiler abstract class.""" + + compiler_type = 'msvc' + + # Private class data (need to distinguish C from C++ source for compiler) + _c_extensions = ['.c'] + _cpp_extensions = ['.cc','.cpp'] + + # Needed for the filename generation methods provided by the + # base class, CCompiler. + src_extensions = _c_extensions + _cpp_extensions + obj_extension = '.obj' + static_lib_extension = '.lib' + shared_lib_extension = '.dll' + static_lib_format = shared_lib_format = '%s%s' + exe_extension = '.exe' + + + def __init__ (self, + verbose=0, + dry_run=0, + force=0): + + CCompiler.__init__ (self, verbose, dry_run, force) + + self.add_library_dir( os.path.join( sys.exec_prefix, 'libs' ) ) + + versions = get_devstudio_versions () + + if versions: + version = versions[0] # highest version + + self.cc = _find_exe("cl.exe", version) + self.link = _find_exe("link.exe", version) + self.lib = _find_exe("lib.exe", version) + set_path_env_var ('lib', version) + set_path_env_var ('include', version) + path=get_msvc_paths('path', version) + try: + for p in string.split(os.environ['path'],';'): + path.append(p) + except KeyError: + pass + os.environ['path'] = string.join(path,';') + else: + # devstudio not found in the registry + self.cc = "cl.exe" + self.link = "link.exe" + self.lib = "lib.exe" + + self.preprocess_options = None + self.compile_options = [ '/nologo', '/Ox', '/MD', '/W3' ] + self.compile_options_debug = ['/nologo', '/Od', '/MDd', '/W3', '/Z7', '/D_DEBUG'] + + self.ldflags_shared = ['/DLL', '/nologo', '/INCREMENTAL:NO'] + self.ldflags_shared_debug = [ + '/DLL', '/nologo', '/INCREMENTAL:no', '/pdb:None', '/DEBUG' + ] + self.ldflags_static = [ '/nologo'] + + + # -- Worker methods ------------------------------------------------ + + def compile (self, + sources, + output_dir=None, + macros=None, + include_dirs=None, + debug=0, + extra_preargs=None, + extra_postargs=None): + + (output_dir, macros, include_dirs) = \ + self._fix_compile_args (output_dir, macros, include_dirs) + (objects, skip_sources) = self._prep_compile (sources, output_dir) + + if extra_postargs is None: + extra_postargs = [] + + pp_opts = gen_preprocess_options (macros, include_dirs) + compile_opts = extra_preargs or [] + compile_opts.append ('/c') + if debug: + compile_opts.extend (self.compile_options_debug) + else: + compile_opts.extend (self.compile_options) + + for i in range (len (sources)): + src = sources[i] ; obj = objects[i] + ext = (os.path.splitext (src))[1] + + if skip_sources[src]: + self.announce ("skipping %s (%s up-to-date)" % (src, obj)) + else: + if ext in self._c_extensions: + input_opt = "/Tc" + src + elif ext in self._cpp_extensions: + input_opt = "/Tp" + src + + output_opt = "/Fo" + obj + + self.mkpath (os.path.dirname (obj)) + self.spawn ([self.cc] + compile_opts + pp_opts + + [input_opt, output_opt] + + extra_postargs) + + return objects + + # compile () + + + def create_static_lib (self, + objects, + output_libname, + output_dir=None, + debug=0, + extra_preargs=None, + extra_postargs=None): + + (objects, output_dir) = \ + self._fix_link_args (objects, output_dir, takes_libs=0) + output_filename = \ + self.library_filename (output_libname, output_dir=output_dir) + + if self._need_link (objects, output_filename): + lib_args = objects + ['/OUT:' + output_filename] + if debug: + pass # XXX what goes here? + if extra_preargs: + lib_args[:0] = extra_preargs + if extra_postargs: + lib_args.extend (extra_postargs) + self.spawn ([self.link] + ld_args) + else: + self.announce ("skipping %s (up-to-date)" % output_filename) + + # create_static_lib () + + + def link_shared_lib (self, + objects, + output_libname, + output_dir=None, + libraries=None, + library_dirs=None, + debug=0, + extra_preargs=None, + extra_postargs=None): + + self.link_shared_object (objects, + self.shared_library_name(output_libname), + output_dir=output_dir, + libraries=libraries, + library_dirs=library_dirs, + debug=debug, + extra_preargs=extra_preargs, + extra_postargs=extra_postargs) + + + def link_shared_object (self, + objects, + output_filename, + output_dir=None, + libraries=None, + library_dirs=None, + debug=0, + extra_preargs=None, + extra_postargs=None): + + (objects, output_dir, libraries, library_dirs) = \ + self._fix_link_args (objects, output_dir, takes_libs=1, + libraries=libraries, library_dirs=library_dirs) + + lib_opts = gen_lib_options (self, + library_dirs, self.runtime_library_dirs, + libraries) + if type (output_dir) not in (StringType, NoneType): + raise TypeError, "'output_dir' must be a string or None" + if output_dir is not None: + output_filename = os.path.join (output_dir, output_filename) + + if self._need_link (objects, output_filename): + + if debug: + ldflags = self.ldflags_shared_debug + # XXX not sure this belongs here + # extensions in debug_mode are named 'module_d.pyd' + basename, ext = os.path.splitext (output_filename) + output_filename = basename + '_d' + ext + else: + ldflags = self.ldflags_shared + + ld_args = ldflags + lib_opts + \ + objects + ['/OUT:' + output_filename] + + if extra_preargs: + ld_args[:0] = extra_preargs + if extra_postargs: + ld_args.extend (extra_postargs) + + self.mkpath (os.path.dirname (output_filename)) + self.spawn ([self.link] + ld_args) + + else: + self.announce ("skipping %s (up-to-date)" % output_filename) + + # link_shared_object () + + + # -- Miscellaneous methods ----------------------------------------- + # These are all used by the 'gen_lib_options() function, in + # ccompiler.py. + + def library_dir_option (self, dir): + return "/LIBPATH:" + dir + + def runtime_library_dir_option (self, dir): + raise DistutilsPlatformError, \ + "don't know how to set runtime library search path for MSVC++" + + def library_option (self, lib): + return self.library_filename (lib) + + + def find_library_file (self, dirs, lib): + + for dir in dirs: + libfile = os.path.join (dir, self.library_filename (lib)) + if os.path.exists (libfile): + return libfile + + else: + # Oops, didn't find it in *any* of 'dirs' + return None + + # find_library_file () + +# class MSVCCompiler diff --git a/distutils/spawn.py b/distutils/spawn.py new file mode 100644 index 00000000..a731f542 --- /dev/null +++ b/distutils/spawn.py @@ -0,0 +1,149 @@ +"""distutils.spawn + +Provides the 'spawn()' function, a front-end to various platform- +specific functions for launching another program in a sub-process.""" + +# created 1999/07/24, Greg Ward + +__revision__ = "$Id$" + +import sys, os, string +from distutils.errors import * + + +def spawn (cmd, + search_path=1, + verbose=0, + dry_run=0): + + """Run another program, specified as a command list 'cmd', in a new + process. 'cmd' is just the argument list for the new process, ie. + cmd[0] is the program to run and cmd[1:] are the rest of its + arguments. There is no way to run a program with a name different + from that of its executable. + + If 'search_path' is true (the default), the system's executable + search path will be used to find the program; otherwise, cmd[0] must + be the exact path to the executable. If 'verbose' is true, a + one-line summary of the command will be printed before it is run. + If 'dry_run' is true, the command will not actually be run. + + Raise DistutilsExecError if running the program fails in any way; + just return on success.""" + + if os.name == 'posix': + _spawn_posix (cmd, search_path, verbose, dry_run) + elif os.name == 'nt': + _spawn_nt (cmd, search_path, verbose, dry_run) + else: + raise DistutilsPlatformError, \ + "don't know how to spawn programs on platform '%s'" % os.name + +# spawn () + + +def _nt_quote_args (args): + """Obscure quoting command line arguments on NT. + Simply quote every argument which contains blanks.""" + + # XXX this doesn't seem very robust to me -- but if the Windows guys + # say it'll work, I guess I'll have to accept it. (What if an arg + # contains quotes? What other magic characters, other than spaces, + # have to be escaped? Is there an escaping mechanism other than + # quoting?) + + for i in range (len (args)): + if string.find (args[i], ' ') == -1: + args[i] = '"%s"' % args[i] + return args + +def _spawn_nt (cmd, + search_path=1, + verbose=0, + dry_run=0): + + executable = cmd[0] + cmd = _nt_quote_args (cmd) + if search_path: + paths = string.split( os.environ['PATH'], os.pathsep) + base,ext = os.path.splitext(executable) + if (ext != '.exe'): + executable = executable + '.exe' + if not os.path.isfile(executable): + paths.reverse() # go over the paths and keep the last one + for p in paths: + f = os.path.join( p, executable ) + if os.path.isfile ( f ): + # the file exists, we have a shot at spawn working + executable = f + if verbose: + print string.join ([executable] + cmd[1:], ' ') + if not dry_run: + # spawn for NT requires a full path to the .exe + try: + rc = os.spawnv (os.P_WAIT, executable, cmd) + except OSError, exc: + # this seems to happen when the command isn't found + raise DistutilsExecError, \ + "command '%s' failed: %s" % (cmd[0], exc[-1]) + if rc != 0: + # and this reflects the command running but failing + raise DistutilsExecError, \ + "command '%s' failed with exit status %d" % (cmd[0], rc) + + + +def _spawn_posix (cmd, + search_path=1, + verbose=0, + dry_run=0): + + if verbose: + print string.join (cmd, ' ') + if dry_run: + return + exec_fn = search_path and os.execvp or os.execv + + pid = os.fork () + + if pid == 0: # in the child + try: + #print "cmd[0] =", cmd[0] + #print "cmd =", cmd + exec_fn (cmd[0], cmd) + except OSError, e: + sys.stderr.write ("unable to execute %s: %s\n" % + (cmd[0], e.strerror)) + os._exit (1) + + sys.stderr.write ("unable to execute %s for unknown reasons" % cmd[0]) + os._exit (1) + + + else: # in the parent + # Loop until the child either exits or is terminated by a signal + # (ie. keep waiting if it's merely stopped) + while 1: + (pid, status) = os.waitpid (pid, 0) + if os.WIFSIGNALED (status): + raise DistutilsExecError, \ + "command '%s' terminated by signal %d" % \ + (cmd[0], os.WTERMSIG (status)) + + elif os.WIFEXITED (status): + exit_status = os.WEXITSTATUS (status) + if exit_status == 0: + return # hey, it succeeded! + else: + raise DistutilsExecError, \ + "command '%s' failed with exit status %d" % \ + (cmd[0], exit_status) + + elif os.WIFSTOPPED (status): + continue + + else: + raise DistutilsExecError, \ + "unknown error executing '%s': termination status %d" % \ + (cmd[0], status) +# _spawn_posix () diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py new file mode 100644 index 00000000..2c7318c0 --- /dev/null +++ b/distutils/sysconfig.py @@ -0,0 +1,266 @@ +"""Provide access to Python's configuration information. The specific names +defined in the module depend heavily on the platform and configuration. + +Written by: Fred L. Drake, Jr. +Email: +Initial date: 17-Dec-1998 +""" + +__version__ = "$Revision$" + +import os +import re +import string +import sys + +from errors import DistutilsPlatformError + + +prefix = os.path.normpath(sys.prefix) +exec_prefix = os.path.normpath(sys.exec_prefix) + + +def get_python_inc(plat_specific=0): + """Return the directory containing installed Python header files. + + If 'plat_specific' is false (the default), this is the path to the + non-platform-specific header files, i.e. Python.h and so on; + otherwise, this is the path to platform-specific header files + (namely config.h). + + """ + the_prefix = (plat_specific and exec_prefix or prefix) + if os.name == "posix": + return os.path.join(the_prefix, "include", "python" + sys.version[:3]) + elif os.name == "nt": + return os.path.join(the_prefix, "Include") # include or Include? + elif os.name == "mac": + return os.path.join(the_prefix, "Include") + else: + raise DistutilsPlatformError, \ + ("I don't know where Python installs its C header files " + + "on platform '%s'") % os.name + + +def get_python_lib(plat_specific=0, standard_lib=0): + """Return the directory containing the Python library (standard or + site additions). + + If 'plat_specific' is true, return the directory containing + platform-specific modules, i.e. any module from a non-pure-Python + module distribution; otherwise, return the platform-shared library + directory. If 'standard_lib' is true, return the directory + containing standard Python library modules; otherwise, return the + directory for site-specific modules. + + """ + the_prefix = (plat_specific and exec_prefix or prefix) + + if os.name == "posix": + libpython = os.path.join(the_prefix, + "lib", "python" + sys.version[:3]) + if standard_lib: + return libpython + else: + return os.path.join(libpython, "site-packages") + + elif os.name == "nt": + if standard_lib: + return os.path.join(the_prefix, "Lib") + else: + return the_prefix + + elif os.name == "mac": + if platform_specific: + if standard_lib: + return os.path.join(exec_prefix, "Mac", "Plugins") + else: + raise DistutilsPlatformError, \ + "OK, where DO site-specific extensions go on the Mac?" + else: + if standard_lib: + return os.path.join(prefix, "Lib") + else: + raise DistutilsPlatformError, \ + "OK, where DO site-specific modules go on the Mac?" + else: + raise DistutilsPlatformError, \ + ("I don't know where Python installs its library " + + "on platform '%s'") % os.name + +# get_python_lib() + + +def get_config_h_filename(): + """Return full pathname of installed config.h file.""" + inc_dir = get_python_inc(plat_specific=1) + return os.path.join(inc_dir, "config.h") + + +def get_makefile_filename(): + """Return full pathname of installed Makefile from the Python build.""" + lib_dir = get_python_lib(plat_specific=1, standard_lib=1) + return os.path.join(lib_dir, "config", "Makefile") + + +def parse_config_h(fp, g=None): + """Parse a config.h-style file. + + A dictionary containing name/value pairs is returned. If an + optional dictionary is passed in as the second argument, it is + used instead of a new dictionary. + """ + if g is None: + g = {} + define_rx = re.compile("#define ([A-Z][A-Z0-9_]+) (.*)\n") + undef_rx = re.compile("/[*] #undef ([A-Z][A-Z0-9_]+) [*]/\n") + # + while 1: + line = fp.readline() + if not line: + break + m = define_rx.match(line) + if m: + n, v = m.group(1, 2) + try: v = string.atoi(v) + except ValueError: pass + g[n] = v + else: + m = undef_rx.match(line) + if m: + g[m.group(1)] = 0 + return g + +def parse_makefile(fp, g=None): + """Parse a Makefile-style file. + + A dictionary containing name/value pairs is returned. If an + optional dictionary is passed in as the second argument, it is + used instead of a new dictionary. + + """ + if g is None: + g = {} + variable_rx = re.compile("([a-zA-Z][a-zA-Z0-9_]+)\s*=\s*(.*)\n") + done = {} + notdone = {} + # + while 1: + line = fp.readline() + if not line: + break + m = variable_rx.match(line) + if m: + n, v = m.group(1, 2) + v = string.strip(v) + if "$" in v: + notdone[n] = v + else: + try: v = string.atoi(v) + except ValueError: pass + done[n] = v + + # do variable interpolation here + findvar1_rx = re.compile(r"\$\(([A-Za-z][A-Za-z0-9_]*)\)") + findvar2_rx = re.compile(r"\${([A-Za-z][A-Za-z0-9_]*)}") + while notdone: + for name in notdone.keys(): + value = notdone[name] + m = findvar1_rx.search(value) + if not m: + m = findvar2_rx.search(value) + if m: + n = m.group(1) + if done.has_key(n): + after = value[m.end():] + value = value[:m.start()] + done[n] + after + if "$" in after: + notdone[name] = value + else: + try: value = string.atoi(value) + except ValueError: pass + done[name] = string.strip(value) + del notdone[name] + elif notdone.has_key(n): + # get it on a subsequent round + pass + else: + done[n] = "" + after = value[m.end():] + value = value[:m.start()] + after + if "$" in after: + notdone[name] = value + else: + try: value = string.atoi(value) + except ValueError: pass + done[name] = string.strip(value) + del notdone[name] + else: + # bogus variable reference; just drop it since we can't deal + del notdone[name] + + # save the results in the global dictionary + g.update(done) + return g + + +def _init_posix(): + """Initialize the module as appropriate for POSIX systems.""" + g = globals() + # load the installed config.h: + parse_config_h(open(get_config_h_filename()), g) + # load the installed Makefile: + parse_makefile(open(get_makefile_filename()), g) + + +def _init_nt(): + """Initialize the module as appropriate for NT""" + g = globals() + # load config.h, though I don't know how useful this is + parse_config_h(open(get_config_h_filename()), g) + # set basic install directories + g['LIBDEST'] = get_python_lib(plat_specific=0, standard_lib=1) + g['BINLIBDEST'] = get_python_lib(plat_specific=1, standard_lib=1) + + # XXX hmmm.. a normal install puts include files here + g['INCLUDEPY'] = get_python_inc(plat_specific=0) + + g['SO'] = '.pyd' + g['exec_prefix'] = exec_prefix + + +def _init_mac(): + """Initialize the module as appropriate for Macintosh systems""" + g = globals() + # load the installed config.h (what if not installed? - still need to + # be able to install packages which don't require compilation) + parse_config_h(open( + os.path.join(sys.exec_prefix, "Mac", "Include", "config.h")), g) + # set basic install directories + g['LIBDEST'] = get_python_lib(plat_specific=0, standard_lib=1) + g['BINLIBDEST'] = get_python_lib(plat_specific=1, standard_lib=1) + + # XXX hmmm.. a normal install puts include files here + g['INCLUDEPY'] = get_python_inc(plat_specific=0) + + g['SO'] = '.ppc.slb' + g['exec_prefix'] = sys.exec_prefix + print sys.prefix + + # XXX are these used anywhere? + g['install_lib'] = os.path.join(sys.exec_prefix, "Lib") + g['install_platlib'] = os.path.join(sys.exec_prefix, "Mac", "Lib") + + +try: + exec "_init_" + os.name +except NameError: + # not needed for this platform + pass +else: + exec "_init_%s()" % os.name + + +del _init_posix +del _init_nt +del _init_mac diff --git a/distutils/text_file.py b/distutils/text_file.py new file mode 100644 index 00000000..7b29ef4a --- /dev/null +++ b/distutils/text_file.py @@ -0,0 +1,355 @@ +"""text_file + +provides the TextFile class, which gives an interface to text files +that (optionally) takes care of stripping comments, ignoring blank +lines, and joining lines with backslashes.""" + +# created 1999/01/12, Greg Ward + +__revision__ = "$Id$" + +from types import * +import sys, os, string, re + + +class TextFile: + + """Provides a file-like object that takes care of all the things you + commonly want to do when processing a text file that has some + line-by-line syntax: strip comments (as long as "#" is your comment + character), skip blank lines, join adjacent lines by escaping the + newline (ie. backslash at end of line), strip leading and/or + trailing whitespace, and collapse internal whitespace. All of these + are optional and independently controllable. + + Provides a 'warn()' method so you can generate warning messages that + report physical line number, even if the logical line in question + spans multiple physical lines. Also provides 'unreadline()' for + implementing line-at-a-time lookahead. + + Constructor is called as: + + TextFile (filename=None, file=None, **options) + + It bombs (RuntimeError) if both 'filename' and 'file' are None; + 'filename' should be a string, and 'file' a file object (or + something that provides 'readline()' and 'close()' methods). It is + recommended that you supply at least 'filename', so that TextFile + can include it in warning messages. If 'file' is not supplied, + TextFile creates its own using the 'open()' builtin. + + The options are all boolean, and affect the value returned by + 'readline()': + strip_comments [default: true] + strip from "#" to end-of-line, as well as any whitespace + leading up to the "#" -- unless it is escaped by a backslash + lstrip_ws [default: false] + strip leading whitespace from each line before returning it + rstrip_ws [default: true] + strip trailing whitespace (including line terminator!) from + each line before returning it + skip_blanks [default: true} + skip lines that are empty *after* stripping comments and + whitespace. (If both lstrip_ws and rstrip_ws are true, + then some lines may consist of solely whitespace: these will + *not* be skipped, even if 'skip_blanks' is true.) + join_lines [default: false] + if a backslash is the last non-newline character on a line + after stripping comments and whitespace, join the following line + to it to form one "logical line"; if N consecutive lines end + with a backslash, then N+1 physical lines will be joined to + form one logical line. + collapse_ws [default: false] + after stripping comments and whitespace and joining physical + lines into logical lines, all internal whitespace (strings of + whitespace surrounded by non-whitespace characters, and not at + the beginning or end of the logical line) will be collapsed + to a single space. + + Note that since 'rstrip_ws' can strip the trailing newline, the + semantics of 'readline()' must differ from those of the builtin file + object's 'readline()' method! In particular, 'readline()' returns + None for end-of-file: an empty string might just be a blank line (or + an all-whitespace line), if 'rstrip_ws' is true but 'skip_blanks' is + not.""" + + default_options = { 'strip_comments': 1, + 'skip_blanks': 1, + 'join_lines': 0, + 'lstrip_ws': 0, + 'rstrip_ws': 1, + 'collapse_ws': 0, + } + + def __init__ (self, filename=None, file=None, **options): + """Construct a new TextFile object. At least one of 'filename' + (a string) and 'file' (a file-like object) must be supplied. + They keyword argument options are described above and affect + the values returned by 'readline()'.""" + + if filename is None and file is None: + raise RuntimeError, \ + "you must supply either or both of 'filename' and 'file'" + + # set values for all options -- either from client option hash + # or fallback to default_options + for opt in self.default_options.keys(): + if options.has_key (opt): + setattr (self, opt, options[opt]) + + else: + setattr (self, opt, self.default_options[opt]) + + # sanity check client option hash + for opt in options.keys(): + if not self.default_options.has_key (opt): + raise KeyError, "invalid TextFile option '%s'" % opt + + if file is None: + self.open (filename) + else: + self.filename = filename + self.file = file + self.current_line = 0 # assuming that file is at BOF! + + # 'linebuf' is a stack of lines that will be emptied before we + # actually read from the file; it's only populated by an + # 'unreadline()' operation + self.linebuf = [] + + + def open (self, filename): + """Open a new file named 'filename'. This overrides both the + 'filename' and 'file' arguments to the constructor.""" + + self.filename = filename + self.file = open (self.filename, 'r') + self.current_line = 0 + + + def close (self): + """Close the current file and forget everything we know about it + (filename, current line number).""" + + self.file.close () + self.file = None + self.filename = None + self.current_line = None + + + def warn (self, msg, line=None): + """Print (to stderr) a warning message tied to the current logical + line in the current file. If the current logical line in the + file spans multiple physical lines, the warning refers to the + whole range, eg. "lines 3-5". If 'line' supplied, it overrides + the current line number; it may be a list or tuple to indicate a + range of physical lines, or an integer for a single physical + line.""" + + if line is None: + line = self.current_line + sys.stderr.write (self.filename + ", ") + if type (line) in (ListType, TupleType): + sys.stderr.write ("lines %d-%d: " % tuple (line)) + else: + sys.stderr.write ("line %d: " % line) + sys.stderr.write (str (msg) + "\n") + + + def readline (self): + """Read and return a single logical line from the current file (or + from an internal buffer if lines have previously been "unread" + with 'unreadline()'). If the 'join_lines' option is true, this + may involve reading multiple physical lines concatenated into a + single string. Updates the current line number, so calling + 'warn()' after 'readline()' emits a warning about the physical + line(s) just read. Returns None on end-of-file, since the empty + string can occur if 'rstrip_ws' is true but 'strip_blanks' is + not.""" + + # If any "unread" lines waiting in 'linebuf', return the top + # one. (We don't actually buffer read-ahead data -- lines only + # get put in 'linebuf' if the client explicitly does an + # 'unreadline()'. + if self.linebuf: + line = self.linebuf[-1] + del self.linebuf[-1] + return line + + buildup_line = '' + + while 1: + # read the line, make it None if EOF + line = self.file.readline() + if line == '': line = None + + if self.strip_comments and line: + + # Look for the first "#" in the line. If none, never + # mind. If we find one and it's the first character, or + # is not preceded by "\", then it starts a comment -- + # strip the comment, strip whitespace before it, and + # carry on. Otherwise, it's just an escaped "#", so + # unescape it (and any other escaped "#"'s that might be + # lurking in there) and otherwise leave the line alone. + + pos = string.find (line, "#") + if pos == -1: # no "#" -- no comments + pass + elif pos == 0 or line[pos-1] != "\\": # it's a comment + + # Have to preserve the trailing newline, because it's + # the job of a later step (rstrip_ws) to remove it -- + # and if rstrip_ws is false, we'd better preserve it! + # (NB. this means that if the final line is all comment + # and has no trailing newline, we will think that it's + # EOF; I think that's OK.) + eol = (line[-1] == '\n') and '\n' or '' + line = line[0:pos] + eol + + else: # it's an escaped "#" + line = string.replace (line, "\\#", "#") + + + # did previous line end with a backslash? then accumulate + if self.join_lines and buildup_line: + # oops: end of file + if line is None: + self.warn ("continuation line immediately precedes " + "end-of-file") + return buildup_line + + line = buildup_line + line + + # careful: pay attention to line number when incrementing it + if type (self.current_line) is ListType: + self.current_line[1] = self.current_line[1] + 1 + else: + self.current_line = [self.current_line, self.current_line+1] + # just an ordinary line, read it as usual + else: + if line is None: # eof + return None + + # still have to be careful about incrementing the line number! + if type (self.current_line) is ListType: + self.current_line = self.current_line[1] + 1 + else: + self.current_line = self.current_line + 1 + + + # strip whitespace however the client wants (leading and + # trailing, or one or the other, or neither) + if self.lstrip_ws and self.rstrip_ws: + line = string.strip (line) + elif self.lstrip_ws: + line = string.lstrip (line) + elif self.rstrip_ws: + line = string.rstrip (line) + + # blank line (whether we rstrip'ed or not)? skip to next line + # if appropriate + if line == '' or line == '\n' and self.skip_blanks: + continue + + if self.join_lines: + if line[-1] == '\\': + buildup_line = line[:-1] + continue + + if line[-2:] == '\\\n': + buildup_line = line[0:-2] + '\n' + continue + + # collapse internal whitespace (*after* joining lines!) + if self.collapse_ws: + line = re.sub (r'(\S)\s+(\S)', r'\1 \2', line) + + # well, I guess there's some actual content there: return it + return line + + # end readline + + + def readlines (self): + """Read and return the list of all logical lines remaining in the + current file.""" + + lines = [] + while 1: + line = self.readline() + if line is None: + return lines + lines.append (line) + + + def unreadline (self, line): + """Push 'line' (a string) onto an internal buffer that will be + checked by future 'readline()' calls. Handy for implementing + a parser with line-at-a-time lookahead.""" + + self.linebuf.append (line) + + +if __name__ == "__main__": + test_data = """# test file + +line 3 \\ +continues on next line +""" + + + # result 1: no fancy options + result1 = map (lambda x: x + "\n", string.split (test_data, "\n")[0:-1]) + + # result 2: just strip comments + result2 = ["\n", "\n", "line 3 \\\n", "continues on next line\n"] + + # result 3: just strip blank lines + result3 = ["# test file\n", "line 3 \\\n", "continues on next line\n"] + + # result 4: default, strip comments, blank lines, and trailing whitespace + result4 = ["line 3 \\", "continues on next line"] + + # result 5: full processing, strip comments and blanks, plus join lines + result5 = ["line 3 continues on next line"] + + def test_input (count, description, file, expected_result): + result = file.readlines () + # result = string.join (result, '') + if result == expected_result: + print "ok %d (%s)" % (count, description) + else: + print "not ok %d (%s):" % (count, description) + print "** expected:" + print expected_result + print "** received:" + print result + + + filename = "test.txt" + out_file = open (filename, "w") + out_file.write (test_data) + out_file.close () + + in_file = TextFile (filename, strip_comments=0, skip_blanks=0, + lstrip_ws=0, rstrip_ws=0) + test_input (1, "no processing", in_file, result1) + + in_file = TextFile (filename, strip_comments=1, skip_blanks=0, + lstrip_ws=0, rstrip_ws=0) + test_input (2, "strip comments", in_file, result2) + + in_file = TextFile (filename, strip_comments=0, skip_blanks=1, + lstrip_ws=0, rstrip_ws=0) + test_input (3, "strip blanks", in_file, result3) + + in_file = TextFile (filename) + test_input (4, "default processing", in_file, result4) + + in_file = TextFile (filename, strip_comments=1, skip_blanks=1, + join_lines=1, rstrip_ws=1) + test_input (5, "full processing", in_file, result5) + + os.remove (filename) + diff --git a/distutils/unixccompiler.py b/distutils/unixccompiler.py new file mode 100644 index 00000000..ec85571d --- /dev/null +++ b/distutils/unixccompiler.py @@ -0,0 +1,304 @@ +"""distutils.unixccompiler + +Contains the UnixCCompiler class, a subclass of CCompiler that handles +the "typical" Unix-style command-line C compiler: + * macros defined with -Dname[=value] + * macros undefined with -Uname + * include search directories specified with -Idir + * libraries specified with -lllib + * library search directories specified with -Ldir + * compile handled by 'cc' (or similar) executable with -c option: + compiles .c to .o + * link static library handled by 'ar' command (possibly with 'ranlib') + * link shared library handled by 'cc -shared' +""" + +# created 1999/07/05, Greg Ward + +__revision__ = "$Id$" + +import string, re, os +from types import * +from copy import copy +from distutils.sysconfig import \ + CC, CCSHARED, CFLAGS, OPT, LDSHARED, LDFLAGS, RANLIB, AR, SO +from distutils.ccompiler import CCompiler, gen_preprocess_options, gen_lib_options + +# XXX Things not currently handled: +# * optimization/debug/warning flags; we just use whatever's in Python's +# Makefile and live with it. Is this adequate? If not, we might +# have to have a bunch of subclasses GNUCCompiler, SGICCompiler, +# SunCCompiler, and I suspect down that road lies madness. +# * even if we don't know a warning flag from an optimization flag, +# we need some way for outsiders to feed preprocessor/compiler/linker +# flags in to us -- eg. a sysadmin might want to mandate certain flags +# via a site config file, or a user might want to set something for +# compiling this module distribution only via the setup.py command +# line, whatever. As long as these options come from something on the +# current system, they can be as system-dependent as they like, and we +# should just happily stuff them into the preprocessor/compiler/linker +# options and carry on. + + +class UnixCCompiler (CCompiler): + + # XXX perhaps there should really be *three* kinds of include + # directories: those built in to the preprocessor, those from Python's + # Makefiles, and those supplied to {add,set}_include_dirs(). Currently + # we make no distinction between the latter two at this point; it's all + # up to the client class to select the include directories to use above + # and beyond the compiler's defaults. That is, both the Python include + # directories and any module- or package-specific include directories + # are specified via {add,set}_include_dirs(), and there's no way to + # distinguish them. This might be a bug. + + compiler_type = 'unix' + + # Needed for the filename generation methods provided by the + # base class, CCompiler. + src_extensions = [".c",".C",".cc",".cxx",".cpp"] + obj_extension = ".o" + static_lib_extension = ".a" + shared_lib_extension = ".so" + static_lib_format = shared_lib_format = "lib%s%s" + + # Command to create a static library: seems to be pretty consistent + # across the major Unices. Might have to move down into the + # constructor if we need platform-specific guesswork. + archiver = "ar" + archiver_options = "-cr" + + + def __init__ (self, + verbose=0, + dry_run=0, + force=0): + + CCompiler.__init__ (self, verbose, dry_run, force) + + self.preprocess_options = None + self.compile_options = None + + # Munge CC and OPT together in case there are flags stuck in CC. + # Note that using these variables from sysconfig immediately makes + # this module specific to building Python extensions and + # inappropriate as a general-purpose C compiler front-end. So sue + # me. Note also that we use OPT rather than CFLAGS, because CFLAGS + # is the flags used to compile Python itself -- not only are there + # -I options in there, they are the *wrong* -I options. We'll + # leave selection of include directories up to the class using + # UnixCCompiler! + + (self.cc, self.ccflags) = \ + _split_command (CC + ' ' + OPT) + self.ccflags_shared = string.split (CCSHARED) + + (self.ld_shared, self.ldflags_shared) = \ + _split_command (LDSHARED) + + self.ld_exec = self.cc + + # __init__ () + + + def compile (self, + sources, + output_dir=None, + macros=None, + include_dirs=None, + debug=0, + extra_preargs=None, + extra_postargs=None): + + (output_dir, macros, include_dirs) = \ + self._fix_compile_args (output_dir, macros, include_dirs) + (objects, skip_sources) = self._prep_compile (sources, output_dir) + + # Figure out the options for the compiler command line. + pp_opts = gen_preprocess_options (macros, include_dirs) + cc_args = ['-c'] + pp_opts + self.ccflags + self.ccflags_shared + if debug: + cc_args[:0] = ['-g'] + if extra_preargs: + cc_args[:0] = extra_preargs + if extra_postargs is None: + extra_postargs = [] + + # Compile all source files that weren't eliminated by + # '_prep_compile()'. + for i in range (len (sources)): + src = sources[i] ; obj = objects[i] + if skip_sources[src]: + self.announce ("skipping %s (%s up-to-date)" % (src, obj)) + else: + self.mkpath (os.path.dirname (obj)) + self.spawn ([self.cc] + cc_args + [src, '-o', obj] + extra_postargs) + + # Return *all* object filenames, not just the ones we just built. + return objects + + # compile () + + + def create_static_lib (self, + objects, + output_libname, + output_dir=None, + debug=0): + + (objects, output_dir) = self._fix_link_args (objects, output_dir, takes_libs=0) + + output_filename = \ + self.library_filename (output_libname, output_dir=output_dir) + + if self._need_link (objects, output_filename): + self.mkpath (os.path.dirname (output_filename)) + self.spawn ([self.archiver, + self.archiver_options, + output_filename] + + objects + self.objects) + else: + self.announce ("skipping %s (up-to-date)" % output_filename) + + # create_static_lib () + + + def link_shared_lib (self, + objects, + output_libname, + output_dir=None, + libraries=None, + library_dirs=None, + debug=0, + extra_preargs=None, + extra_postargs=None): + self.link_shared_object ( + objects, + self.shared_library_filename (output_libname), + output_dir, + libraries, + library_dirs, + debug, + extra_preargs, + extra_postargs) + + + def link_shared_object (self, + objects, + output_filename, + output_dir=None, + libraries=None, + library_dirs=None, + debug=0, + extra_preargs=None, + extra_postargs=None): + + (objects, output_dir, libraries, library_dirs) = \ + self._fix_link_args (objects, output_dir, takes_libs=1, + libraries=libraries, library_dirs=library_dirs) + + lib_opts = gen_lib_options (self, + library_dirs, self.runtime_library_dirs, + libraries) + if type (output_dir) not in (StringType, NoneType): + raise TypeError, "'output_dir' must be a string or None" + if output_dir is not None: + output_filename = os.path.join (output_dir, output_filename) + + if self._need_link (objects, output_filename): + ld_args = (self.ldflags_shared + objects + self.objects + + lib_opts + ['-o', output_filename]) + if debug: + ld_args[:0] = ['-g'] + if extra_preargs: + ld_args[:0] = extra_preargs + if extra_postargs: + ld_args.extend (extra_postargs) + self.mkpath (os.path.dirname (output_filename)) + self.spawn ([self.ld_shared] + ld_args) + else: + self.announce ("skipping %s (up-to-date)" % output_filename) + + # link_shared_object () + + + def link_executable (self, + objects, + output_progname, + output_dir=None, + libraries=None, + library_dirs=None, + debug=0, + extra_preargs=None, + extra_postargs=None): + + (objects, output_dir, libraries, library_dirs) = \ + self._fix_link_args (objects, output_dir, takes_libs=1, + libraries=libraries, library_dirs=library_dirs) + + lib_opts = gen_lib_options (self, + library_dirs, self.runtime_library_dirs, + libraries) + output_filename = output_progname # Unix-ism! + if output_dir is not None: + output_filename = os.path.join (output_dir, output_filename) + + if self._need_link (objects, output_filename): + ld_args = objects + self.objects + lib_opts + ['-o', output_filename] + if debug: + ld_args[:0] = ['-g'] + if extra_preargs: + ld_args[:0] = extra_preargs + if extra_postargs: + ld_args.extend (extra_postargs) + self.mkpath (os.path.dirname (output_filename)) + self.spawn ([self.ld_exec] + ld_args) + else: + self.announce ("skipping %s (up-to-date)" % output_filename) + + # link_executable () + + + # -- Miscellaneous methods ----------------------------------------- + # These are all used by the 'gen_lib_options() function, in + # ccompiler.py. + + def library_dir_option (self, dir): + return "-L" + dir + + def runtime_library_dir_option (self, dir): + return "-R" + dir + + def library_option (self, lib): + return "-l" + lib + + + def find_library_file (self, dirs, lib): + + for dir in dirs: + shared = os.path.join (dir, self.shared_library_filename (lib)) + static = os.path.join (dir, self.library_filename (lib)) + + # We're second-guessing the linker here, with not much hard + # data to go on: GCC seems to prefer the shared library, so I'm + # assuming that *all* Unix C compilers do. And of course I'm + # ignoring even GCC's "-static" option. So sue me. + if os.path.exists (shared): + return shared + elif os.path.exists (static): + return static + + else: + # Oops, didn't find it in *any* of 'dirs' + return None + + # find_library_file () + +# class UnixCCompiler + + +def _split_command (cmd): + """Split a command string up into the progam to run (a string) and + the list of arguments; return them as (cmd, arglist).""" + args = string.split (cmd) + return (args[0], args[1:]) diff --git a/distutils/util.py b/distutils/util.py new file mode 100644 index 00000000..f271eafb --- /dev/null +++ b/distutils/util.py @@ -0,0 +1,528 @@ +"""distutils.util + +General-purpose utility functions used throughout the Distutils +(especially in command classes). Mostly filesystem manipulation, but +not limited to that. The functions in this module generally raise +DistutilsFileError when they have problems with the filesystem, because +os.error in pre-1.5.2 Python only gives the error message and not the +file causing it.""" + +# created 1999/03/08, Greg Ward + +__revision__ = "$Id$" + +import os, string, shutil, sys +from distutils.errors import * + + +# cache for by mkpath() -- in addition to cheapening redundant calls, +# eliminates redundant "creating /foo/bar/baz" messages in dry-run mode +PATH_CREATED = {} + +# I don't use os.makedirs because a) it's new to Python 1.5.2, and +# b) it blows up if the directory already exists (I want to silently +# succeed in that case). +def mkpath (name, mode=0777, verbose=0, dry_run=0): + """Create a directory and any missing ancestor directories. If the + directory already exists, return silently. Raise + DistutilsFileError if unable to create some directory along the + way (eg. some sub-path exists, but is a file rather than a + directory). If 'verbose' is true, print a one-line summary of + each mkdir to stdout.""" + + global PATH_CREATED + + # XXX what's the better way to handle verbosity? print as we create + # each directory in the path (the current behaviour), or only announce + # the creation of the whole path? (quite easy to do the latter since + # we're not using a recursive algorithm) + + name = os.path.normpath (name) + created_dirs = [] + if os.path.isdir (name) or name == '': + return created_dirs + if PATH_CREATED.get (name): + return created_dirs + + (head, tail) = os.path.split (name) + tails = [tail] # stack of lone dirs to create + + while head and tail and not os.path.isdir (head): + #print "splitting '%s': " % head, + (head, tail) = os.path.split (head) + #print "to ('%s','%s')" % (head, tail) + tails.insert (0, tail) # push next higher dir onto stack + + #print "stack of tails:", tails + + # now 'head' contains the deepest directory that already exists + # (that is, the child of 'head' in 'name' is the highest directory + # that does *not* exist) + for d in tails: + #print "head = %s, d = %s: " % (head, d), + head = os.path.join (head, d) + if PATH_CREATED.get (head): + continue + + if verbose: + print "creating", head + + if not dry_run: + try: + os.mkdir (head) + created_dirs.append(head) + except os.error, (errno, errstr): + raise DistutilsFileError, "'%s': %s" % (head, errstr) + + PATH_CREATED[head] = 1 + return created_dirs + +# mkpath () + + +def newer (source, target): + """Return true if 'source' exists and is more recently modified than + 'target', or if 'source' exists and 'target' doesn't. Return + false if both exist and 'target' is the same age or younger than + 'source'. Raise DistutilsFileError if 'source' does not + exist.""" + + if not os.path.exists (source): + raise DistutilsFileError, "file '%s' does not exist" % source + if not os.path.exists (target): + return 1 + + from stat import ST_MTIME + mtime1 = os.stat(source)[ST_MTIME] + mtime2 = os.stat(target)[ST_MTIME] + + return mtime1 > mtime2 + +# newer () + + +def newer_pairwise (sources, targets): + """Walk two filename lists in parallel, testing if each source is newer + than its corresponding target. Return a pair of lists (sources, + targets) where source is newer than target, according to the + semantics of 'newer()'.""" + + if len (sources) != len (targets): + raise ValueError, "'sources' and 'targets' must be same length" + + # build a pair of lists (sources, targets) where source is newer + n_sources = [] + n_targets = [] + for i in range (len (sources)): + if newer (sources[i], targets[i]): + n_sources.append (sources[i]) + n_targets.append (targets[i]) + + return (n_sources, n_targets) + +# newer_pairwise () + + +def newer_group (sources, target, missing='error'): + """Return true if 'target' is out-of-date with respect to any + file listed in 'sources'. In other words, if 'target' exists and + is newer than every file in 'sources', return false; otherwise + return true. 'missing' controls what we do when a source file is + missing; the default ("error") is to blow up with an OSError from + inside 'stat()'; if it is "ignore", we silently drop any missing + source files; if it is "newer", any missing source files make us + assume that 'target' is out-of-date (this is handy in "dry-run" + mode: it'll make you pretend to carry out commands that wouldn't + work because inputs are missing, but that doesn't matter because + you're not actually going to run the commands).""" + + # If the target doesn't even exist, then it's definitely out-of-date. + if not os.path.exists (target): + return 1 + + # Otherwise we have to find out the hard way: if *any* source file + # is more recent than 'target', then 'target' is out-of-date and + # we can immediately return true. If we fall through to the end + # of the loop, then 'target' is up-to-date and we return false. + from stat import ST_MTIME + target_mtime = os.stat (target)[ST_MTIME] + for source in sources: + if not os.path.exists (source): + if missing == 'error': # blow up when we stat() the file + pass + elif missing == 'ignore': # missing source dropped from + continue # target's dependency list + elif missing == 'newer': # missing source means target is + return 1 # out-of-date + + source_mtime = os.stat(source)[ST_MTIME] + if source_mtime > target_mtime: + return 1 + else: + return 0 + +# newer_group () + + +# XXX this isn't used anywhere, and worse, it has the same name as a method +# in Command with subtly different semantics. (This one just has one +# source -> one dest; that one has many sources -> one dest.) Nuke it? +def make_file (src, dst, func, args, + verbose=0, update_message=None, noupdate_message=None): + """Makes 'dst' from 'src' (both filenames) by calling 'func' with + 'args', but only if it needs to: i.e. if 'dst' does not exist or + 'src' is newer than 'dst'.""" + + if newer (src, dst): + if verbose and update_message: + print update_message + apply (func, args) + else: + if verbose and noupdate_message: + print noupdate_message + +# make_file () + + +def _copy_file_contents (src, dst, buffer_size=16*1024): + """Copy the file 'src' to 'dst'; both must be filenames. Any error + opening either file, reading from 'src', or writing to 'dst', + raises DistutilsFileError. Data is read/written in chunks of + 'buffer_size' bytes (default 16k). No attempt is made to handle + anything apart from regular files.""" + + # Stolen from shutil module in the standard library, but with + # custom error-handling added. + + fsrc = None + fdst = None + try: + try: + fsrc = open(src, 'rb') + except os.error, (errno, errstr): + raise DistutilsFileError, \ + "could not open '%s': %s" % (src, errstr) + + try: + fdst = open(dst, 'wb') + except os.error, (errno, errstr): + raise DistutilsFileError, \ + "could not create '%s': %s" % (dst, errstr) + + while 1: + try: + buf = fsrc.read (buffer_size) + except os.error, (errno, errstr): + raise DistutilsFileError, \ + "could not read from '%s': %s" % (src, errstr) + + if not buf: + break + + try: + fdst.write(buf) + except os.error, (errno, errstr): + raise DistutilsFileError, \ + "could not write to '%s': %s" % (dst, errstr) + + finally: + if fdst: + fdst.close() + if fsrc: + fsrc.close() + +# _copy_file_contents() + + +def copy_file (src, dst, + preserve_mode=1, + preserve_times=1, + update=0, + verbose=0, + dry_run=0): + + """Copy a file 'src' to 'dst'. If 'dst' is a directory, then 'src' + is copied there with the same name; otherwise, it must be a + filename. (If the file exists, it will be ruthlessly clobbered.) + If 'preserve_mode' is true (the default), the file's mode (type + and permission bits, or whatever is analogous on the current + platform) is copied. If 'preserve_times' is true (the default), + the last-modified and last-access times are copied as well. If + 'update' is true, 'src' will only be copied if 'dst' does not + exist, or if 'dst' does exist but is older than 'src'. If + 'verbose' is true, then a one-line summary of the copy will be + printed to stdout. + + Return true if the file was copied (or would have been copied), + false otherwise (ie. 'update' was true and the destination is + up-to-date).""" + + # XXX doesn't copy Mac-specific metadata + + from stat import * + + if not os.path.isfile (src): + raise DistutilsFileError, \ + "can't copy '%s': not a regular file" % src + + if os.path.isdir (dst): + dir = dst + dst = os.path.join (dst, os.path.basename (src)) + else: + dir = os.path.dirname (dst) + + if update and not newer (src, dst): + if verbose: + print "not copying %s (output up-to-date)" % src + return 0 + + if verbose: + print "copying %s -> %s" % (src, dir) + + if dry_run: + return 1 + + # On a Mac, use the native file copy routine + if os.name == 'mac': + import macostools + try: + macostools.copy (src, dst, 0, preserve_times) + except OSError, exc: + raise DistutilsFileError, \ + "could not copy '%s' to '%s': %s" % (src, dst, exc[-1]) + return 1 + + # Otherwise use custom routine + _copy_file_contents (src, dst) + if preserve_mode or preserve_times: + st = os.stat (src) + + # According to David Ascher , utime() should be done + # before chmod() (at least under NT). + if preserve_times: + os.utime (dst, (st[ST_ATIME], st[ST_MTIME])) + if preserve_mode: + os.chmod (dst, S_IMODE (st[ST_MODE])) + + return 1 + +# copy_file () + + +def copy_tree (src, dst, + preserve_mode=1, + preserve_times=1, + preserve_symlinks=0, + update=0, + verbose=0, + dry_run=0): + + + """Copy an entire directory tree 'src' to a new location 'dst'. Both + 'src' and 'dst' must be directory names. If 'src' is not a + directory, raise DistutilsFileError. If 'dst' does not exist, it + is created with 'mkpath()'. The end result of the copy is that + every file in 'src' is copied to 'dst', and directories under + 'src' are recursively copied to 'dst'. Return the list of files + copied (under their output names) -- note that if 'update' is true, + this might be less than the list of files considered. Return + value is not affected by 'dry_run'. + + 'preserve_mode' and 'preserve_times' are the same as for + 'copy_file'; note that they only apply to regular files, not to + directories. If 'preserve_symlinks' is true, symlinks will be + copied as symlinks (on platforms that support them!); otherwise + (the default), the destination of the symlink will be copied. + 'update' and 'verbose' are the same as for 'copy_file'.""" + + if not dry_run and not os.path.isdir (src): + raise DistutilsFileError, \ + "cannot copy tree '%s': not a directory" % src + try: + names = os.listdir (src) + except os.error, (errno, errstr): + if dry_run: + names = [] + else: + raise DistutilsFileError, \ + "error listing files in '%s': %s" % (src, errstr) + outputs = [] + + if not dry_run: + outputs.extend(mkpath (dst, verbose=verbose)) + + + for n in names: + src_name = os.path.join (src, n) + dst_name = os.path.join (dst, n) + + if preserve_symlinks and os.path.islink (src_name): + link_dest = os.readlink (src_name) + if verbose: + print "linking %s -> %s" % (dst_name, link_dest) + if not dry_run: + os.symlink (link_dest, dst_name) + outputs.append (dst_name) + + elif os.path.isdir (src_name): + outputs.extend ( + copy_tree (src_name, dst_name, + preserve_mode, preserve_times, preserve_symlinks, + update, verbose, dry_run)) + else: + if (copy_file (src_name, dst_name, + preserve_mode, preserve_times, + update, verbose, dry_run)): + outputs.append (dst_name) + + return outputs + +# copy_tree () + + +def remove_tree (directory, verbose=0, dry_run=0): + """Recursively remove an entire directory tree. Any errors are ignored + (apart from being reported to stdout if 'verbose' is true).""" + + if verbose: + print "removing '%s' (and everything under it)" % directory + if dry_run: + return + try: + shutil.rmtree(directory,1) + except (IOError, OSError), exc: + if verbose: + if exc.filename: + print "error removing %s: %s (%s)" % \ + (directory, exc.strerror, exc.filename) + else: + print "error removing %s: %s" % (directory, exc.strerror) + + +# XXX I suspect this is Unix-specific -- need porting help! +def move_file (src, dst, + verbose=0, + dry_run=0): + + """Move a file 'src' to 'dst'. If 'dst' is a directory, the file + will be moved into it with the same name; otherwise, 'src' is + just renamed to 'dst'. Return the new full name of the file. + + Handles cross-device moves on Unix using + 'copy_file()'. What about other systems???""" + + from os.path import exists, isfile, isdir, basename, dirname + + if verbose: + print "moving %s -> %s" % (src, dst) + + if dry_run: + return dst + + if not isfile (src): + raise DistutilsFileError, \ + "can't move '%s': not a regular file" % src + + if isdir (dst): + dst = os.path.join (dst, basename (src)) + elif exists (dst): + raise DistutilsFileError, \ + "can't move '%s': destination '%s' already exists" % \ + (src, dst) + + if not isdir (dirname (dst)): + raise DistutilsFileError, \ + "can't move '%s': destination '%s' not a valid path" % \ + (src, dst) + + copy_it = 0 + try: + os.rename (src, dst) + except os.error, (num, msg): + if num == errno.EXDEV: + copy_it = 1 + else: + raise DistutilsFileError, \ + "couldn't move '%s' to '%s': %s" % (src, dst, msg) + + if copy_it: + copy_file (src, dst) + try: + os.unlink (src) + except os.error, (num, msg): + try: + os.unlink (dst) + except os.error: + pass + raise DistutilsFileError, \ + ("couldn't move '%s' to '%s' by copy/delete: " + + "delete '%s' failed: %s") % \ + (src, dst, src, msg) + + return dst + +# move_file () + + +def write_file (filename, contents): + """Create a file with the specified name and write 'contents' (a + sequence of strings without line terminators) to it.""" + + f = open (filename, "w") + for line in contents: + f.write (line + "\n") + f.close () + + +def get_platform (): + """Return a string (suitable for tacking onto directory names) that + identifies the current platform. Under Unix, identifies both the OS + and hardware architecture, e.g. "linux-i586", "solaris-sparc", + "irix-mips". For Windows and Mac OS, just returns 'sys.platform' -- + i.e. "???" or "???".""" + + if os.name == 'posix': + uname = os.uname() + OS = uname[0] + arch = uname[4] + return "%s-%s" % (string.lower (OS), string.lower (arch)) + else: + return sys.platform + +# get_platform() + + +def native_path (pathname): + """Return 'pathname' as a name that will work on the native + filesystem, i.e. split it on '/' and put it back together again + using the current directory separator. Needed because filenames in + the setup script are always supplied in Unix style, and have to be + converted to the local convention before we can actually use them in + the filesystem. Raises DistutilsValueError if 'pathname' is + absolute (starts with '/') or contains local directory separators + (unless the local separator is '/', of course).""" + + if pathname[0] == '/': + raise DistutilsValueError, "path '%s' cannot be absolute" % pathname + if pathname[-1] == '/': + raise DistutilsValueError, "path '%s' cannot end with '/'" % pathname + if os.sep != '/': + if os.sep in pathname: + raise DistutilsValueError, \ + "path '%s' cannot contain '%c' character" % \ + (pathname, os.sep) + + paths = string.split (pathname, '/') + return apply (os.path.join, paths) + else: + return pathname + +# native_path () + +def add_path_prefix(prefix, path): + return prefix+os.path.splitdrive(path)[1] + +def remove_path_prefix(prefix, path): + drive = os.path.splitdrive(path)[0] + return drive + path[len(prefix):] + diff --git a/distutils/version.py b/distutils/version.py new file mode 100644 index 00000000..918a1ded --- /dev/null +++ b/distutils/version.py @@ -0,0 +1,301 @@ +# +# distutils/version.py +# +# Implements multiple version numbering conventions for the +# Python Module Distribution Utilities. +# +# written by Greg Ward, 1998/12/17 +# +# $Id$ +# + +"""Provides classes to represent module version numbers (one class for +each style of version numbering). There are currently two such classes +implemented: StrictVersion and LooseVersion. + +Every version number class implements the following interface: + * the 'parse' method takes a string and parses it to some internal + representation; if the string is an invalid version number, + 'parse' raises a ValueError exception + * the class constructor takes an optional string argument which, + if supplied, is passed to 'parse' + * __str__ reconstructs the string that was passed to 'parse' (or + an equivalent string -- ie. one that will generate an equivalent + version number instance) + * __repr__ generates Python code to recreate the version number instance + * __cmp__ compares the current instance with either another instance + of the same class or a string (which will be parsed to an instance + of the same class, thus must follow the same rules) +""" + +import string, re +from types import StringType + +class Version: + """Abstract base class for version numbering classes. Just provides + constructor (__init__) and reproducer (__repr__), because those + seem to be the same for all version numbering classes. + """ + + def __init__ (self, vstring=None): + if vstring: + self.parse (vstring) + + def __repr__ (self): + return "%s ('%s')" % (self.__class__.__name__, str (self)) + + +# Interface for version-number classes -- must be implemented +# by the following classes (the concrete ones -- Version should +# be treated as an abstract class). +# __init__ (string) - create and take same action as 'parse' +# (string parameter is optional) +# parse (string) - convert a string representation to whatever +# internal representation is appropriate for +# this style of version numbering +# __str__ (self) - convert back to a string; should be very similar +# (if not identical to) the string supplied to parse +# __repr__ (self) - generate Python code to recreate +# the instance +# __cmp__ (self, other) - compare two version numbers ('other' may +# be an unparsed version string, or another +# instance of your version class) + + +class StrictVersion (Version): + + """Version numbering for anal retentives and software idealists. + Implements the standard interface for version number classes as + described above. A version number consists of two or three + dot-separated numeric components, with an optional "pre-release" tag + on the end. The pre-release tag consists of the letter 'a' or 'b' + followed by a number. If the numeric components of two version + numbers are equal, then one with a pre-release tag will always + be deemed earlier (lesser) than one without. + + The following are valid version numbers (shown in the order that + would be obtained by sorting according to the supplied cmp function): + + 0.4 0.4.0 (these two are equivalent) + 0.4.1 + 0.5a1 + 0.5b3 + 0.5 + 0.9.6 + 1.0 + 1.0.4a3 + 1.0.4b1 + 1.0.4 + + The following are examples of invalid version numbers: + + 1 + 2.7.2.2 + 1.3.a4 + 1.3pl1 + 1.3c4 + + The rationale for this version numbering system will be explained + in the distutils documentation. + """ + + version_re = re.compile (r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$', + re.VERBOSE) + + + def parse (self, vstring): + match = self.version_re.match (vstring) + if not match: + raise ValueError, "invalid version number '%s'" % vstring + + (major, minor, patch, prerelease, prerelease_num) = \ + match.group (1, 2, 4, 5, 6) + + if patch: + self.version = tuple (map (string.atoi, [major, minor, patch])) + else: + self.version = tuple (map (string.atoi, [major, minor]) + [0]) + + if prerelease: + self.prerelease = (prerelease[0], string.atoi (prerelease_num)) + else: + self.prerelease = None + + + def __str__ (self): + + if self.version[2] == 0: + vstring = string.join (map (str, self.version[0:2]), '.') + else: + vstring = string.join (map (str, self.version), '.') + + if self.prerelease: + vstring = vstring + self.prerelease[0] + str (self.prerelease[1]) + + return vstring + + + def __cmp__ (self, other): + if isinstance (other, StringType): + other = StrictVersion (other) + + compare = cmp (self.version, other.version) + if (compare == 0): # have to compare prerelease + + # case 1: neither has prerelease; they're equal + # case 2: self has prerelease, other doesn't; other is greater + # case 3: self doesn't have prerelease, other does: self is greater + # case 4: both have prerelease: must compare them! + + if (not self.prerelease and not other.prerelease): + return 0 + elif (self.prerelease and not other.prerelease): + return -1 + elif (not self.prerelease and other.prerelease): + return 1 + elif (self.prerelease and other.prerelease): + return cmp (self.prerelease, other.prerelease) + + else: # numeric versions don't match -- + return compare # prerelease stuff doesn't matter + + +# end class StrictVersion + + +# The rules according to Greg Stein: +# 1) a version number has 1 or more numbers separate by a period or by +# sequences of letters. If only periods, then these are compared +# left-to-right to determine an ordering. +# 2) sequences of letters are part of the tuple for comparison and are +# compared lexicographically +# 3) recognize the numeric components may have leading zeroes +# +# The LooseVersion class below implements these rules: a version number +# string is split up into a tuple of integer and string components, and +# comparison is a simple tuple comparison. This means that version +# numbers behave in a predictable and obvious way, but a way that might +# not necessarily be how people *want* version numbers to behave. There +# wouldn't be a problem if people could stick to purely numeric version +# numbers: just split on period and compare the numbers as tuples. +# However, people insist on putting letters into their version numbers; +# the most common purpose seems to be: +# - indicating a "pre-release" version +# ('alpha', 'beta', 'a', 'b', 'pre', 'p') +# - indicating a post-release patch ('p', 'pl', 'patch') +# but of course this can't cover all version number schemes, and there's +# no way to know what a programmer means without asking him. +# +# The problem is what to do with letters (and other non-numeric +# characters) in a version number. The current implementation does the +# obvious and predictable thing: keep them as strings and compare +# lexically within a tuple comparison. This has the desired effect if +# an appended letter sequence implies something "post-release": +# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002". +# +# However, if letters in a version number imply a pre-release version, +# the "obvious" thing isn't correct. Eg. you would expect that +# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison +# implemented here, this just isn't so. +# +# Two possible solutions come to mind. The first is to tie the +# comparison algorithm to a particular set of semantic rules, as has +# been done in the StrictVersion class above. This works great as long +# as everyone can go along with bondage and discipline. Hopefully a +# (large) subset of Python module programmers will agree that the +# particular flavour of bondage and discipline provided by StrictVersion +# provides enough benefit to be worth using, and will submit their +# version numbering scheme to its domination. The free-thinking +# anarchists in the lot will never give in, though, and something needs +# to be done to accomodate them. +# +# Perhaps a "moderately strict" version class could be implemented that +# lets almost anything slide (syntactically), and makes some heuristic +# assumptions about non-digits in version number strings. This could +# sink into special-case-hell, though; if I was as talented and +# idiosyncratic as Larry Wall, I'd go ahead and implement a class that +# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is +# just as happy dealing with things like "2g6" and "1.13++". I don't +# think I'm smart enough to do it right though. +# +# In any case, I've coded the test suite for this module (see +# ../test/test_version.py) specifically to fail on things like comparing +# "1.2a2" and "1.2". That's not because the *code* is doing anything +# wrong, it's because the simple, obvious design doesn't match my +# complicated, hairy expectations for real-world version numbers. It +# would be a snap to fix the test suite to say, "Yep, LooseVersion does +# the Right Thing" (ie. the code matches the conception). But I'd rather +# have a conception that matches common notions about version numbers. + +class LooseVersion (Version): + + """Version numbering for anarchists and software realists. + Implements the standard interface for version number classes as + described above. A version number consists of a series of numbers, + separated by either periods or strings of letters. When comparing + version numbers, the numeric components will be compared + numerically, and the alphabetic components lexically. The following + are all valid version numbers, in no particular order: + + 1.5.1 + 1.5.2b2 + 161 + 3.10a + 8.02 + 3.4j + 1996.07.12 + 3.2.pl0 + 3.1.1.6 + 2g6 + 11g + 0.960923 + 2.2beta29 + 1.13++ + 5.5.kw + 2.0b1pl0 + + In fact, there is no such thing as an invalid version number under + this scheme; the rules for comparison are simple and predictable, + but may not always give the results you want (for some definition + of "want"). + """ + + component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE) + + def __init__ (self, vstring=None): + if vstring: + self.parse (vstring) + + + def parse (self, vstring): + # I've given up on thinking I can reconstruct the version string + # from the parsed tuple -- so I just store the string here for + # use by __str__ + self.vstring = vstring + components = filter (lambda x: x and x != '.', + self.component_re.split (vstring)) + for i in range (len (components)): + try: + components[i] = int (components[i]) + except ValueError: + pass + + self.version = components + + + def __str__ (self): + return self.vstring + + + def __repr__ (self): + return "LooseVersion ('%s')" % str (self) + + + def __cmp__ (self, other): + if isinstance (other, StringType): + other = LooseVersion (other) + + return cmp (self.version, other.version) + + +# end class LooseVersion