import os, sys, struct, re from distutils import sysconfig, util from distutils.ccompiler import new_compiler, show_compilers from distutils.core import Command from distutils.dep_util import newer, newer_group from Structures import Script, ExecutableScript, Executable STUB_MAIN_SOURCE = os.path.join(os.path.dirname(__file__), 'stubmain.c') SHELL_SCRIPT_HEAD = """#!%(env_executable)s%(env_args)s%(py_executable)s # Force Python to ignore the current directory when importing the # module(s) used in the script body. import sys, imp module_info = imp.find_module('Ft', filter(None, sys.path)) imp.load_module('Ft', *module_info) ### SCRIPT BODY ### """ COMMAND_LINE_APP_BODY = """# script generated in %s %%(import_stmt)s %%(classname)s().run() """ % __name__ class BuildScripts(Command): command_name = 'build_scripts' description = "\"build\" scripts" user_options = [ ('build-dir=', 'd', "directory to \"build\" (copy) to"), ('build-temp=', 't', "directory for temporary files (build by-products)"), ('force', 'f', "forcibly build everything (ignore file timestamps"), ('debug', 'g', "compile/link with debugging information"), ('compiler=', 'c', "specify the compiler type"), ] help_options = [ ('help-compiler', None, "list available compilers", show_compilers), ] boolean_options = ['force', 'debug'] stub_name = 'stubmain' def initialize_options(self): self.build_dir = None self.build_temp = None self.force = None self.debug = None self.compiler = None return def finalize_options(self): undefined_temp = self.build_temp is None self.set_undefined_options('build', ('build_scripts', 'build_dir'), ('build_temp', 'build_temp'), ('compiler', 'compiler'), ('debug', 'debug'), ('force', 'force')) if undefined_temp: self.build_temp = os.path.join(self.build_temp, 'scripts') self.scripts = self.distribution.scripts or [] # Get the linker arguments for building executables if os.name == 'posix': args = sysconfig.get_config_vars('LDFLAGS', 'LINKFORSHARED') self.link_preargs = ' '.join(args).split() args = sysconfig.get_config_vars('LIBS', 'MODLIBS', 'SYSLIBS', 'LDLAST') self.link_postargs = ' '.join(args).split() else: self.link_preargs = [] self.link_postargs = [] # Get the extension for executables (only if needed for the OS) if os.name == 'nt' or sys.platform.startswith('darwin'): self.exe_extension = sysconfig.get_config_var('EXE') or '' else: self.exe_extension = '' if os.name == 'nt': if self.debug: self.exe_extension = '_d' + self.exe_extension stub_exe = self.stub_name + self.exe_extension self.win32_stub_filename = os.path.join(self.build_temp, stub_exe) else: self.win32_stub_filename = None return def run(self): """ Create the proper script for the current platform. """ if not self.scripts: return # Ensure the destination directory exists self.mkpath(self.build_dir) # Break out the various types of scripts scripts = [ x for x in self.scripts if isinstance(x, Script) ] executables = [ x for x in self.scripts if isinstance(x, Executable) ] exec_scripts = [ x for x in self.scripts if isinstance(x, ExecutableScript) ] # Create the compiler for any Windows script, executables or # executable scripts if (os.name == 'nt' and (scripts or exec_scripts)) or executables: self._prep_compiler() # Create the executable stub for any Windows script or executable # scripts if os.name == 'nt' and (scripts or exec_scripts): self._prep_win32_stub() for script in scripts: self.build_script(script) for executable in executables: self.build_executable(executable) for exec_script in exec_scripts: self.build_executable_script(exec_script) return # -- worker functions --------------------------------------------- def build_script(self, script): """ Builds a CommandLineApp script. On POSIX systems, this is a generated shell script. For Windows, it is a compiled executable with the generated file appended to the end of the stub. """ # Create the script body import_stmt = 'import ' + script.identifier if script.module: import_stmt = 'from ' + script.module + ' ' + import_stmt repl = {'classname' : script.identifier, 'import_stmt' : import_stmt, } self.write_script(script, COMMAND_LINE_APP_BODY % repl) return def build_executable_script(self, exec_script): """ Builds a hand-written shell script. On POSIX systems, this is nothing more than a file copy. For Windows, the executable stub has the contents of the script file appended to the end of the stub. """ # Read in the script body source = util.convert_path(exec_script.source) f = open(source, 'r') script_body = f.read() f.close() self.write_script(exec_script, script_body, [source]) return def build_executable(self, executable): """ Builds a compiled executable. For all systems, the executable is created in the same fashion as the Python interpreter executable. """ outfile = self.get_script_filename(executable) all_sources = self._prep_build(script) sources = [] for source, includes in all_sources: sources.append(source) sources.extend(includes) if not (self.force or newer_group(sources, outfile, 'newer')): self.announce("skipping '%s' executable (up-to-date)" % executable.name) return else: self.announce("building '%s' executable" % executable.name) output_dir = os.path.join(self.build_temp, executable.name) macros = executable.define_macros[:] for undef in executable.undef_macros: macros.append((undef,)) objects = [] for source, includes in all_sources: if not self.force: # Recompile if the includes or source are newer than the # resulting object files. objs = self.compiler.object_filenames([source], 1, output_dir) # Recompile if any of the inputs are newer than the object inputs = [source] + includes force = 0 for filename in objs: force = force or newer_group(inputs, filename, 'newer') self.compiler.force = force objs = self.compiler.compile( [source], output_dir=output_dir, macros=macros, include_dirs=executable.include_dirs, debug=self.debug, extra_postargs=executable.extra_compile_args) objects.extend(objs) # Reset the force flag on the compiler self.compiler.force = self.force # Now link the object files together into a "shared object" -- # of course, first we have to figure out all the other things # that go into the mix. if os.name == 'nt' and self.debug: executable = executable.name + '_d' else: executable = executable.name if executable.extra_objects: objects.extend(executable.extra_objects) # On Windows, non-MSVC compilers need some help finding python # libs. This logic comes from distutils/command/build_ext.py. libraries = executable.libraries if sys.platform == "win32": from distutils.msvccompiler import MSVCCompiler if not isinstance(self.compiler, MSVCCompiler): template = "python%d%d" if self.debug: template = template + "_d" pythonlib = (template % (sys.hexversion >> 24, (sys.hexversion >> 16) & 0xff)) libraries += [pythonlib] self.compiler.link_executable( objects, executable, libraries=libraries, library_dirs=executable.library_dirs, runtime_library_dirs=executable.runtime_library_dirs, extra_preargs=self.link_preargs, extra_postargs=self.link_postargs + executable.extra_link_args, debug=self.debug, build_temp=self.build_temp) return # -- utility functions -------------------------------------------- def write_script(self, script, script_body, extra_sources=None): # Get the destination filename outfile = self.get_script_filename(script) # Get the list of source dependencies for this script sources = [__file__] if os.name == 'nt': sources.append(self.win32_stub_filename) if extra_sources: sources.extend(extra_sources) # Determine if the script needs to be built if not (self.force or newer_group(sources, outfile, 'newer')): self.announce("skipping '%s' script (up-to-date)" % script.name) return else: self.announce("building '%s' script" % script.name) if self.dry_run: # Don't actually create the script return # prepare args for the bang path at the top of the script ENV_BIN = '/usr/bin/env' if os.name == 'posix': # Some Solaris platforms may not have an 'env' binary. # If /usr/bin/env exists, use '#!/usr/bin/env python' # otherwise, use '#!' + sys.executable env_exec = os.path.isfile(ENV_BIN) and \ os.access(ENV_BIN, os.X_OK) and ENV_BIN or '' env_args = '' py_exec = env_exec and 'python' or sys.executable else: # shouldn't matter on non-POSIX; we'll just use defaults env_exec = ENV_BIN env_args = '' py_exec = 'python' head_args = { 'env_executable': env_exec, 'env_args': env_exec and (' %s' % env_args) or '', 'py_executable': py_exec } script = SHELL_SCRIPT_HEAD % head_args + script_body if os.name == 'posix': # Create the file with execute permissions set fd = os.open(outfile, os.O_WRONLY|os.O_CREAT|os.O_TRUNC, 0755) os.write(fd, script) os.close(fd) elif os.name == 'nt': # Get the stub executable data f = open(self.win32_stub_filename, 'rb') stub_data = f.read() f.close() # append the data to the stub executable f = open(outfile, 'wb') # Write the executable f.write(stub_data) # Write the script_data struct # struct script_data { # int tag; # int length; # } script_data = struct.pack("ii", 0x1234567A, len(script)) f.write(script_data) # Write the script string f.write(script) # Write the script_info struct # struct script_info { # int tag; # int offset; # } script_info = struct.pack("ii", 0x1234567A, len(stub_data)) f.write(script_info) f.close() else: raise DistutilsPlatformError, \ "don't know how to build scripts on platform %r" % os.name return def get_script_filename(self, script): """ Convert the name of a script into the name of the file which it will be run from. """ # All Windows scripts are executables if os.name == 'nt' or isinstance(script, Executable): script_name = script.name + self.exe_extension else: script_name = script.name return os.path.join(self.build_dir, script_name) # -- helper functions --------------------------------------------- def _prep_compiler(self): # Setup the CCompiler object that we'll use to do all the # compiling and linking self.compiler = new_compiler(compiler=self.compiler, verbose=self.verbose, dry_run=self.dry_run, force=self.force) sysconfig.customize_compiler(self.compiler) # Make sure Python's include directories (for Python.h, pyconfig.h, # etc.) are in the include search path. py_include = sysconfig.get_python_inc() plat_py_include = sysconfig.get_python_inc(plat_specific=1) include_dirs = [py_include] if plat_py_include != py_include: include_dirs.append(plat_py_include) self.compiler.set_include_dirs(include_dirs) if os.name == 'posix': # Add the Python archive library ldlibrary = sysconfig.get_config_var('BLDLIBRARY') # MacOSX with frameworks doesn't link against a library if ldlibrary: # Get the location of the library file for d in sysconfig.get_config_vars('LIBDIR', 'LIBP', 'LIBPL'): library = os.path.join(d, ldlibrary) if os.path.exists(library): self.compiler.add_link_object(library) break elif os.name == 'nt': # Add Python's library directory lib_dir = os.path.join(sys.exec_prefix, 'libs') self.compiler.add_library_dir(lib_dir) return def _prep_win32_stub(self): if self.force or newer(STUB_MAIN_SOURCE, self.win32_stub_filename): # Create the executable stub self.announce("building stub executable") # Force rebuild of scripts self.force = 1 if os.name == 'nt' and self.debug: executable = self.stub_name + '_d' else: executable = self.stub_name objects = self.compiler.compile([STUB_MAIN_SOURCE], output_dir=self.build_temp, debug=self.debug) # On Windows, non-MSVC compilers need some help finding python # libs. This logic comes from distutils/command/build_ext.py. libraries = [] if sys.platform == "win32": from distutils.msvccompiler import MSVCCompiler if not isinstance(self.compiler, MSVCCompiler): template = "python%d%d" if self.debug: template = template + "_d" pythonlib = (template % (sys.hexversion >> 24, (sys.hexversion >> 16) & 0xff)) libraries.append(pythonlib) self.compiler.link_executable(objects, executable, libraries=libraries, output_dir=self.build_temp, debug=self.debug, extra_preargs=self.link_preargs, extra_postargs=self.link_postargs) else: self.announce("skipping stub executable (up-to-date)") return _include_re = re.compile(r'^\s*#\s*include\s*("|<)(?P[^">]+?)("|>)', re.MULTILINE) def _find_includes(self, script, filename, includes): source = open(filename).read() basename = os.path.basename(filename) include_dirs = [os.path.dirname(filename)] include_dirs.extend(script.include_dirs) match = self._include_re.search(source) while match: filename = util.convert_path(match.group('file')) for base in include_dirs: include = os.path.normpath(os.path.join(base, filename)) if os.path.isfile(include) and include not in includes: includes.append(include) self._find_includes(script, include, includes) break match = self._include_re.search(source, match.end()) return def _prep_build(self, script): # This should really exist in the CCompiler class, but # that would required overriding all compilers. result = [] for source in script.sources: includes = [] self._find_includes(script, util.convert_path(source), includes) result.append((source, includes)) return result # -- external interfaces ------------------------------------------ def get_outputs(self): outfiles = [] for script in self.scripts: outfiles.append(self.get_script_filename(script)) return outfiles def get_source_files(self): filenames = [] for script in self.scripts: if isinstance(script, ExecutableScript): filenames.append(script.source) return filenames