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 = 0x00005446 # "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
|