import os import sys import time from distutils import spawn, sysconfig, util from distutils.ccompiler import new_compiler, show_compilers from distutils.core import Command from distutils.dep_util import newer_group from Ft.Lib import ImportUtil from Ft.Lib.DistExt import Util, ImageHlp from Ft.Lib.DistExt.Structures import Script, Executable SHELL_SCRIPT_BODY = """#!%(executable)s # %(name)s script generated by %(command)s on %(timestamp)s. # DO NOT EDIT THIS FILE! import %(module)s status = %(module)s.%(function)s() raise SystemExit(status) """ class ScriptInfo(ImageHlp.Struct): """ Representation of the SCRIPT_INFO resource in the stub executable. """ __fields__ = [ (ImageHlp.Dword, 'Signature'), (ImageHlp.Word, 'MajorPythonVersion'), (ImageHlp.Word, 'MinorPythonVersion'), (ImageHlp.Word, 'Subsystem'), (ImageHlp.Word, 'Characteristics'), (ImageHlp.Word, 'ScriptAddress'), (ImageHlp.Word, 'ScriptSize'), ] 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'] 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 self.exe_extension = sysconfig.get_config_var('EXE') or '' if self.debug and os.name == 'nt': self.exe_extension = '_d' + self.exe_extension 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) # Build the "plain" (pure-Python) scripts. self.build_scripts([ script for script in self.scripts if isinstance(script, Script) ]) # Build the executable (compiled) scripts. self.build_executables([ script for script in self.scripts if isinstance(script, Executable) ]) return # -- worker functions --------------------------------------------- def build_scripts(self, scripts): for script in scripts: self.build_script(script) return 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. """ # Get the destination filename outfile = self.get_script_filename(script) # Determine if the script needs to be built command_mtime = ImportUtil.GetLastModified(__name__) if os.name == 'nt': stub_mtime = ImportUtil.GetResourceLastModified(__name__, 'stubmain.exe') command_mtime = max(command_mtime, stub_mtime) try: target_mtime = os.stat(outfile).st_mtime except OSError: target_mtime = -1 if not (self.force or command_mtime > target_mtime): self.announce("skipping '%s' script (up-to-date)" % script.name) return else: self.announce("building '%s' script" % (script.name), 2) repl = {'executable' : self.get_python_executable(), 'command' : self.get_command_name(), 'timestamp' : time.asctime(), 'toplevel' : script.module.split('.', 1)[0], } repl.update(vars(script)) script_body = SHELL_SCRIPT_BODY % repl if self.dry_run: # Don't actually create the script pass elif os.name == 'nt': # Populate the ScriptInfo structure script_info = ScriptInfo() script_info.Signature = 0x00004654 # "FT\0\0" script_info.MajorPythonVersion = sys.version_info[0] script_info.MinorPythonVersion = sys.version_info[1] script_info.Subsystem = 0x0003; # CUI if self.debug: script_info.Characteristics |= 0x0001 stub_bytes = ImportUtil.GetResourceString(__name__, 'stubmain.exe') script_info.ScriptAddress = len(stub_bytes) script_info.ScriptSize = len(script_body) # Write the script executable f = open(outfile, 'w+b') try: f.write(stub_bytes) f.write(script_body) ImageHlp.UpdateResource(f, ImageHlp.RT_RCDATA, 1, script_info) ImageHlp.SetSubsystem(f, ImageHlp.IMAGE_SUBSYSTEM_WINDOWS_CUI) finally: f.close() else: # Create the file with execute permissions set fd = os.open(outfile, os.O_WRONLY|os.O_CREAT|os.O_TRUNC, 0755) try: os.write(fd, script_body) finally: os.close(fd) return def build_executables(self, executables): if not executables: return # Create the compiler for compiling the executables. self._prep_compiler() for executable in executables: self.build_executable(executable) 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 get_python_executable(self): if os.name == 'nt': executable = sys.executable else: executable = spawn.find_executable('env') if executable is None: # No 'env' executable found; use the interpreter directly executable = sys.executable else: # Use the python found runtime (via env) executable += ' python' return executable 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) # If we were asked to build any C/C++ libraries, make sure that the # directory where we put them is in the library search path for # linking executables. if self.distribution.has_c_libraries(): build_clib = self.get_finalized_command('build_clib') self.compiler.set_libraries(build_clib.get_library_names()) self.compiler.add_library_dir(build_clib.build_clib) # 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) self.compiler.add_include_dir(py_include) if plat_py_include != py_include: include_dirs.append(plat_py_include) 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_build(self, script): # This should really exist in the CCompiler class, but # that would required overriding all compilers. result = [] for source in script.sources: source = util.convert_path(source) includes = Util.FindIncludes(source, script.include_dirs) result.append((source, includes)) return result # -- external interfaces ------------------------------------------ def get_outputs(self): return [ self.get_script_filename(script) for script in self.scripts ] def get_source_files(self): filenames = [] for script in self.scripts: if isinstance(script, Executable): for source, includes in self._prep_build(script): filenames.append(source) filenames.extend(includes) return filenames