2
0
mirror of https://gitlab.com/ita1024/waf.git synced 2024-11-29 21:41:44 +01:00
waf/waflib/Configure.py
Alexander Afanasyev 9b75b843ae Tools/python: Extending python tool
add: show more command-line arguments to control which
     python is used and where py/pyext files should be installed.
     The new options are in addition to the existing PYTHON, PYTHONDIR,
     and PYTHONARCHDIR environment variable, providing the help messages
     to the user, so it can easily change parameters without second-guessing
     of what are they.

     Environmental parameters override command line arguments (is it right?)

updated: An extension to Configure.find_command to allow specification
         of the name for the searched command (using `msg` option). This is
         helpful when the search command is a huge list, as in the case of
         python's python-config case.

updated: Do not specify prefix for `get_python_lib`, since this way it
         can result in a wrong PYTHONDIR and PYTHONARCHDIR on certain platforms
         (e.g., OSX)

updated: Changing logic with byte-compilation of python files. Mow, files are
         compiled during the build stage, not install stage.  During the
         install stage files are simply copied to the requested
         location.

updated: If there is an error in python source code, byte-compilation
         phase will raise an exception and abort building process.

Signed-off-by: Thomas Nagy <tnagy2pow10@gmail.com>
2013-08-17 18:15:05 +02:00

576 lines
16 KiB
Python

#!/usr/bin/env python
# encoding: utf-8
# Thomas Nagy, 2005-2010 (ita)
"""
Configuration system
A :py:class:`waflib.Configure.ConfigurationContext` instance is created when ``waf configure`` is called, it is used to:
* create data dictionaries (ConfigSet instances)
* store the list of modules to import
* hold configuration routines such as ``find_program``, etc
"""
import os, shlex, sys, time
from waflib import ConfigSet, Utils, Options, Logs, Context, Build, Errors
try:
from urllib import request
except ImportError:
from urllib import urlopen
else:
urlopen = request.urlopen
BREAK = 'break'
"""In case of a configuration error, break"""
CONTINUE = 'continue'
"""In case of a configuration error, continue"""
WAF_CONFIG_LOG = 'config.log'
"""Name of the configuration log file"""
autoconfig = False
"""Execute the configuration automatically"""
conf_template = '''# project %(app)s configured on %(now)s by
# waf %(wafver)s (abi %(abi)s, python %(pyver)x on %(systype)s)
# using %(args)s
#'''
def download_check(node):
"""
Hook to check for the tools which are downloaded. Replace with your function if necessary.
"""
pass
def download_tool(tool, force=False, ctx=None):
"""
Download a Waf tool from the remote repository defined in :py:const:`waflib.Context.remote_repo`::
$ waf configure --download
"""
for x in Utils.to_list(Context.remote_repo):
for sub in Utils.to_list(Context.remote_locs):
url = '/'.join((x, sub, tool + '.py'))
try:
web = urlopen(url)
try:
if web.getcode() != 200:
continue
except AttributeError:
pass
except Exception:
# on python3 urlopen throws an exception
# python 2.3 does not have getcode and throws an exception to fail
continue
else:
tmp = ctx.root.make_node(os.sep.join((Context.waf_dir, 'waflib', 'extras', tool + '.py')))
tmp.write(web.read(), 'wb')
Logs.warn('Downloaded %s from %s' % (tool, url))
download_check(tmp)
try:
module = Context.load_tool(tool)
except Exception:
Logs.warn('The tool %s from %s is unusable' % (tool, url))
try:
tmp.delete()
except Exception:
pass
continue
return module
raise Errors.WafError('Could not load the Waf tool')
class ConfigurationContext(Context.Context):
'''configures the project'''
cmd = 'configure'
error_handlers = []
"""
Additional functions to handle configuration errors
"""
def __init__(self, **kw):
super(ConfigurationContext, self).__init__(**kw)
self.environ = dict(os.environ)
self.all_envs = {}
self.top_dir = None
self.out_dir = None
self.tools = [] # tools loaded in the configuration, and that will be loaded when building
self.hash = 0
self.files = []
self.tool_cache = []
self.setenv('')
def setenv(self, name, env=None):
"""
Set a new config set for conf.env. If a config set of that name already exists,
recall it without modification.
The name is the filename prefix to save to ``c4che/NAME_cache.py``, and it
is also used as *variants* by the build commands.
Though related to variants, whatever kind of data may be stored in the config set::
def configure(cfg):
cfg.env.ONE = 1
cfg.setenv('foo')
cfg.env.ONE = 2
def build(bld):
2 == bld.env_of_name('foo').ONE
:param name: name of the configuration set
:type name: string
:param env: ConfigSet to copy, or an empty ConfigSet is created
:type env: :py:class:`waflib.ConfigSet.ConfigSet`
"""
if name not in self.all_envs or env:
if not env:
env = ConfigSet.ConfigSet()
self.prepare_env(env)
else:
env = env.derive()
self.all_envs[name] = env
self.variant = name
def get_env(self):
"""Getter for the env property"""
return self.all_envs[self.variant]
def set_env(self, val):
"""Setter for the env property"""
self.all_envs[self.variant] = val
env = property(get_env, set_env)
def init_dirs(self):
"""
Initialize the project directory and the build directory
"""
top = self.top_dir
if not top:
top = Options.options.top
if not top:
top = getattr(Context.g_module, Context.TOP, None)
if not top:
top = self.path.abspath()
top = os.path.abspath(top)
self.srcnode = (os.path.isabs(top) and self.root or self.path).find_dir(top)
assert(self.srcnode)
out = self.out_dir
if not out:
out = Options.options.out
if not out:
out = getattr(Context.g_module, Context.OUT, None)
if not out:
out = Options.lockfile.replace('.lock-waf_%s_' % sys.platform, '').replace('.lock-waf', '')
self.bldnode = (os.path.isabs(out) and self.root or self.path).make_node(out)
self.bldnode.mkdir()
if not os.path.isdir(self.bldnode.abspath()):
conf.fatal('Could not create the build directory %s' % self.bldnode.abspath())
def execute(self):
"""
See :py:func:`waflib.Context.Context.execute`
"""
self.init_dirs()
self.cachedir = self.bldnode.make_node(Build.CACHE_DIR)
self.cachedir.mkdir()
path = os.path.join(self.bldnode.abspath(), WAF_CONFIG_LOG)
self.logger = Logs.make_logger(path, 'cfg')
app = getattr(Context.g_module, 'APPNAME', '')
if app:
ver = getattr(Context.g_module, 'VERSION', '')
if ver:
app = "%s (%s)" % (app, ver)
now = time.ctime()
pyver = sys.hexversion
systype = sys.platform
args = " ".join(sys.argv)
wafver = Context.WAFVERSION
abi = Context.ABI
self.to_log(conf_template % vars())
self.msg('Setting top to', self.srcnode.abspath())
self.msg('Setting out to', self.bldnode.abspath())
if id(self.srcnode) == id(self.bldnode):
Logs.warn('Setting top == out (remember to use "update_outputs")')
elif id(self.path) != id(self.srcnode):
if self.srcnode.is_child_of(self.path):
Logs.warn('Are you certain that you do not want to set top="." ?')
super(ConfigurationContext, self).execute()
self.store()
Context.top_dir = self.srcnode.abspath()
Context.out_dir = self.bldnode.abspath()
# this will write a configure lock so that subsequent builds will
# consider the current path as the root directory (see prepare_impl).
# to remove: use 'waf distclean'
env = ConfigSet.ConfigSet()
env['argv'] = sys.argv
env['options'] = Options.options.__dict__
env.run_dir = Context.run_dir
env.top_dir = Context.top_dir
env.out_dir = Context.out_dir
# conf.hash & conf.files hold wscript files paths and hash
# (used only by Configure.autoconfig)
env['hash'] = self.hash
env['files'] = self.files
env['environ'] = dict(self.environ)
if not self.env.NO_LOCK_IN_RUN:
env.store(Context.run_dir + os.sep + Options.lockfile)
if not self.env.NO_LOCK_IN_TOP:
env.store(Context.top_dir + os.sep + Options.lockfile)
if not self.env.NO_LOCK_IN_OUT:
env.store(Context.out_dir + os.sep + Options.lockfile)
def prepare_env(self, env):
"""
Insert *PREFIX*, *BINDIR* and *LIBDIR* values into ``env``
:type env: :py:class:`waflib.ConfigSet.ConfigSet`
:param env: a ConfigSet, usually ``conf.env``
"""
if not env.PREFIX:
if Options.options.prefix or Utils.is_win32:
env.PREFIX = os.path.abspath(os.path.expanduser(Options.options.prefix))
else:
env.PREFIX = ''
if not env.BINDIR:
env.BINDIR = Utils.subst_vars('${PREFIX}/bin', env)
if not env.LIBDIR:
env.LIBDIR = Utils.subst_vars('${PREFIX}/lib', env)
def store(self):
"""Save the config results into the cache file"""
n = self.cachedir.make_node('build.config.py')
n.write('version = 0x%x\ntools = %r\n' % (Context.HEXVERSION, self.tools))
if not self.all_envs:
self.fatal('nothing to store in the configuration context!')
for key in self.all_envs:
tmpenv = self.all_envs[key]
tmpenv.store(os.path.join(self.cachedir.abspath(), key + Build.CACHE_SUFFIX))
def load(self, input, tooldir=None, funs=None, download=True):
"""
Load Waf tools, which will be imported whenever a build is started.
:param input: waf tools to import
:type input: list of string
:param tooldir: paths for the imports
:type tooldir: list of string
:param funs: functions to execute from the waf tools
:type funs: list of string
:param download: whether to download the tool from the waf repository
:type download: bool
"""
tools = Utils.to_list(input)
if tooldir: tooldir = Utils.to_list(tooldir)
for tool in tools:
# avoid loading the same tool more than once with the same functions
# used by composite projects
mag = (tool, id(self.env), funs)
if mag in self.tool_cache:
self.to_log('(tool %s is already loaded, skipping)' % tool)
continue
self.tool_cache.append(mag)
module = None
try:
module = Context.load_tool(tool, tooldir)
except ImportError as e:
if Options.options.download:
module = download_tool(tool, ctx=self)
if not module:
self.fatal('Could not load the Waf tool %r or download a suitable replacement from the repository (sys.path %r)\n%s' % (tool, sys.path, e))
else:
self.fatal('Could not load the Waf tool %r from %r (try the --download option?):\n%s' % (tool, sys.path, e))
except Exception as e:
self.to_log('imp %r (%r & %r)' % (tool, tooldir, funs))
self.to_log(Utils.ex_stack())
raise
if funs is not None:
self.eval_rules(funs)
else:
func = getattr(module, 'configure', None)
if func:
if type(func) is type(Utils.readf): func(self)
else: self.eval_rules(func)
self.tools.append({'tool':tool, 'tooldir':tooldir, 'funs':funs})
def post_recurse(self, node):
"""
Records the path and a hash of the scripts visited, see :py:meth:`waflib.Context.Context.post_recurse`
:param node: script
:type node: :py:class:`waflib.Node.Node`
"""
super(ConfigurationContext, self).post_recurse(node)
self.hash = hash((self.hash, node.read('rb')))
self.files.append(node.abspath())
def eval_rules(self, rules):
"""
Execute the configuration tests. The method :py:meth:`waflib.Configure.ConfigurationContext.err_handler`
is used to process the eventual exceptions
:param rules: list of configuration method names
:type rules: list of string
"""
self.rules = Utils.to_list(rules)
for x in self.rules:
f = getattr(self, x)
if not f: self.fatal("No such method '%s'." % x)
try:
f()
except Exception as e:
ret = self.err_handler(x, e)
if ret == BREAK:
break
elif ret == CONTINUE:
continue
else:
raise
def err_handler(self, fun, error):
"""
Error handler for the configuration tests, the default is to let the exception raise
:param fun: configuration test
:type fun: method
:param error: exception
:type error: exception
"""
pass
def conf(f):
"""
Decorator: attach new configuration functions to :py:class:`waflib.Build.BuildContext` and
:py:class:`waflib.Configure.ConfigurationContext`. The methods bound will accept a parameter
named 'mandatory' to disable the configuration errors::
def configure(conf):
conf.find_program('abc', mandatory=False)
:param f: method to bind
:type f: function
"""
def fun(*k, **kw):
mandatory = True
if 'mandatory' in kw:
mandatory = kw['mandatory']
del kw['mandatory']
try:
return f(*k, **kw)
except Errors.ConfigurationError:
if mandatory:
raise
setattr(ConfigurationContext, f.__name__, fun)
setattr(Build.BuildContext, f.__name__, fun)
return f
@conf
def add_os_flags(self, var, dest=None):
"""
Import operating system environment values into ``conf.env`` dict::
def configure(conf):
conf.add_os_flags('CFLAGS')
:param var: variable to use
:type var: string
:param dest: destination variable, by default the same as var
:type dest: string
"""
# do not use 'get' to make certain the variable is not defined
try: self.env.append_value(dest or var, shlex.split(self.environ[var]))
except KeyError: pass
@conf
def cmd_to_list(self, cmd):
"""
Detect if a command is written in pseudo shell like ``ccache g++`` and return a list.
:param cmd: command
:type cmd: a string or a list of string
"""
if isinstance(cmd, str) and cmd.find(' '):
try:
os.stat(cmd)
except OSError:
return shlex.split(cmd)
else:
return [cmd]
return cmd
@conf
def check_waf_version(self, mini='1.6.99', maxi='1.8.0'):
"""
Raise a Configuration error if the Waf version does not strictly match the given bounds::
conf.check_waf_version(mini='1.7.0', maxi='1.8.0')
:type mini: number, tuple or string
:param mini: Minimum required version
:type maxi: number, tuple or string
:param maxi: Maximum allowed version
"""
self.start_msg('Checking for waf version in %s-%s' % (str(mini), str(maxi)))
ver = Context.HEXVERSION
if Utils.num2ver(mini) > ver:
self.fatal('waf version should be at least %r (%r found)' % (Utils.num2ver(mini), ver))
if Utils.num2ver(maxi) < ver:
self.fatal('waf version should be at most %r (%r found)' % (Utils.num2ver(maxi), ver))
self.end_msg('ok')
@conf
def find_file(self, filename, path_list=[]):
"""
Find a file in a list of paths
:param filename: name of the file to search for
:param path_list: list of directories to search
:return: the first occurrence filename or '' if filename could not be found
"""
for n in Utils.to_list(filename):
for d in Utils.to_list(path_list):
p = os.path.join(d, n)
if os.path.exists(p):
return p
self.fatal('Could not find %r' % filename)
@conf
def find_program(self, filename, **kw):
"""
Search for a program on the operating system
When var is used, you may set os.environ[var] to help find a specific program version, for example::
$ VALAC=/usr/bin/valac_test waf configure
:param path_list: paths to use for searching
:type param_list: list of string
:param var: store the result to conf.env[var], by default use filename.upper()
:type var: string
:param ext: list of extensions for the binary (do not add an extension for portability)
:type ext: list of string
:param msg: name to display in the log, by default filename is used
:type msg: string
"""
exts = kw.get('exts', Utils.is_win32 and '.exe,.com,.bat,.cmd' or ',.sh,.pl,.py')
environ = kw.get('environ', os.environ)
ret = ''
filename = Utils.to_list(filename)
var = kw.get('var', '')
if not var:
var = filename[0].upper()
if self.env[var]:
ret = self.env[var]
elif var in environ:
ret = environ[var]
path_list = kw.get('path_list', '')
if not ret:
if path_list:
path_list = Utils.to_list(path_list)
else:
path_list = environ.get('PATH', '').split(os.pathsep)
if not isinstance(filename, list):
filename = [filename]
for a in exts.split(','):
if ret:
break
for b in filename:
if ret:
break
for c in path_list:
if ret:
break
x = os.path.expanduser(os.path.join(c, b + a))
if os.path.isfile(x):
ret = x
if not ret and Utils.winreg:
ret = Utils.get_registry_app_path(Utils.winreg.HKEY_CURRENT_USER, filename)
if not ret and Utils.winreg:
ret = Utils.get_registry_app_path(Utils.winreg.HKEY_LOCAL_MACHINE, filename)
msg = kw.get('msg', ', '.join(filename))
self.msg("Checking for program '%s'" % msg, ret or False)
self.to_log('find program=%r paths=%r var=%r -> %r' % (filename, path_list, var, ret))
if not ret:
self.fatal(kw.get('errmsg', '') or 'Could not find the program %s' % ','.join(filename))
if var:
self.env[var] = ret
return ret
@conf
def find_perl_program(self, filename, path_list=[], var=None, environ=None, exts=''):
"""
Search for a perl program on the operating system
:param filename: file to search for
:type filename: string
:param path_list: list of paths to look into
:type path_list: list of string
:param var: store the results into *conf.env.var*
:type var: string
:param environ: operating system environment to pass to :py:func:`waflib.Configure.find_program`
:type environ: dict
:param exts: extensions given to :py:func:`waflib.Configure.find_program`
:type exts: list
"""
try:
app = self.find_program(filename, path_list=path_list, var=var, environ=environ, exts=exts)
except Exception:
self.find_program('perl', var='PERL')
app = self.find_file(filename, os.environ['PATH'].split(os.pathsep))
if not app:
raise
if var:
self.env[var] = Utils.to_list(self.env['PERL']) + [app]
self.msg('Checking for %r' % filename, app)