diff options
author | David Robillard <d@drobilla.net> | 2018-09-11 20:54:08 +0200 |
---|---|---|
committer | David Robillard <d@drobilla.net> | 2018-09-11 20:54:08 +0200 |
commit | 62dcd3f33c7f8ba0f5694cc88b36f685463dac5b (patch) | |
tree | 5bb277b47d04635c5a38fb4da83e69c2242afef6 /waflib/Configure.py | |
parent | c4dcd9609a4e0a968c58830301cfb49e2beffd4b (diff) | |
parent | b6e9de2de9725e2f5a3170b8171ad1a1e95e8339 (diff) | |
download | pugl-62dcd3f33c7f8ba0f5694cc88b36f685463dac5b.tar.gz pugl-62dcd3f33c7f8ba0f5694cc88b36f685463dac5b.tar.bz2 pugl-62dcd3f33c7f8ba0f5694cc88b36f685463dac5b.zip |
Merge commit 'b6e9de2de9725e2f5a3170b8171ad1a1e95e8339' as 'waflib'
Diffstat (limited to 'waflib/Configure.py')
-rw-r--r-- | waflib/Configure.py | 638 |
1 files changed, 638 insertions, 0 deletions
diff --git a/waflib/Configure.py b/waflib/Configure.py new file mode 100644 index 0000000..d0a4793 --- /dev/null +++ b/waflib/Configure.py @@ -0,0 +1,638 @@ +#!/usr/bin/env python +# encoding: utf-8 +# Thomas Nagy, 2005-2018 (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, re, shlex, shutil, sys, time, traceback +from waflib import ConfigSet, Utils, Options, Logs, Context, Build, Errors + +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 +#''' + +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', '') + + # someone can be messing with symlinks + out = os.path.realpath(out) + + 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) + + params = {'now': time.ctime(), 'pyver': sys.hexversion, 'systype': sys.platform, 'args': " ".join(sys.argv), 'wafver': Context.WAFVERSION, 'abi': Context.ABI, 'app': app} + self.to_log(conf_template % params) + 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') + 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.config_cmd = self.cmd + + 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 or env.environ.get('NO_LOCK_IN_RUN') or getattr(Options.options, 'no_lock_in_run')): + env.store(os.path.join(Context.run_dir, Options.lockfile)) + if not (self.env.NO_LOCK_IN_TOP or env.environ.get('NO_LOCK_IN_TOP') or getattr(Options.options, 'no_lock_in_top')): + env.store(os.path.join(Context.top_dir, Options.lockfile)) + if not (self.env.NO_LOCK_IN_OUT or env.environ.get('NO_LOCK_IN_OUT') or getattr(Options.options, 'no_lock_in_out')): + env.store(os.path.join(Context.out_dir, 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 = Options.options.prefix + else: + env.PREFIX = '/' + if not env.BINDIR: + if Options.options.bindir: + env.BINDIR = Options.options.bindir + else: + env.BINDIR = Utils.subst_vars('${PREFIX}/bin', env) + if not env.LIBDIR: + if Options.options.libdir: + env.LIBDIR = Options.options.libdir + else: + env.LIBDIR = Utils.subst_vars('${PREFIX}/lib%s' % Utils.lib64(), 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, tool_list, tooldir=None, funs=None, with_sys_path=True, cache=False): + """ + Load Waf tools, which will be imported whenever a build is started. + + :param tool_list: waf tools to import + :type tool_list: 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 cache: whether to prevent the tool from running twice + :type cache: bool + """ + + tools = Utils.to_list(tool_list) + 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 + + if cache: + mag = (tool, id(self.env), tooldir, 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, ctx=self, with_sys_path=with_sys_path) + except ImportError as e: + self.fatal('Could not load the Waf tool %r from %r\n%s' % (tool, getattr(e, 'waf_sys_path', sys.path), e)) + except Exception as e: + self.to_log('imp %r (%r & %r)' % (tool, tooldir, funs)) + self.to_log(traceback.format_exc()) + 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 = Utils.h_list((self.hash, node.read('rb'))) + self.files.append(node.abspath()) + + def eval_rules(self, rules): + """ + Execute configuration tests provided as list of functions to run + + :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 configuration function %r' % x) + f() + +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 = kw.pop('mandatory', True) + try: + return f(*k, **kw) + except Errors.ConfigurationError: + if mandatory: + raise + + fun.__name__ = f.__name__ + setattr(ConfigurationContext, f.__name__, fun) + setattr(Build.BuildContext, f.__name__, fun) + return f + +@conf +def add_os_flags(self, var, dest=None, dup=False): + """ + 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 + :param dup: add the same set of flags again + :type dup: bool + """ + try: + flags = shlex.split(self.environ[var]) + except KeyError: + return + if dup or ''.join(flags) not in ''.join(Utils.to_list(self.env[dest or var])): + self.env.append_value(dest or var, flags) + +@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): + if os.path.isfile(cmd): + # do not take any risk + return [cmd] + if os.sep == '/': + return shlex.split(cmd) + else: + try: + return shlex.split(cmd, posix=False) + except TypeError: + # Python 2.5 on windows? + return shlex.split(cmd) + return cmd + +@conf +def check_waf_version(self, mini='1.9.99', maxi='2.1.0', **kw): + """ + Raise a Configuration error if the Waf version does not strictly match the given bounds:: + + conf.check_waf_version(mini='1.9.99', maxi='2.1.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)), **kw) + 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', **kw) + +@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 matching filename; else a configuration exception is raised + """ + for n in Utils.to_list(filename): + for d in Utils.to_list(path_list): + p = os.path.expanduser(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:: + + $ CC='ccache gcc' 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] where var defaults to filename.upper() if not provided; the result is stored as a list of strings + :type var: string + :param value: obtain the program from the value passed exclusively + :type value: list or string (list is preferred) + :param exts: list of extensions for the binary (do not add an extension for portability) + :type exts: list of string + :param msg: name to display in the log, by default filename is used + :type msg: string + :param interpreter: interpreter for the program + :type interpreter: ConfigSet variable key + :raises: :py:class:`waflib.Errors.ConfigurationError` + """ + + exts = kw.get('exts', Utils.is_win32 and '.exe,.com,.bat,.cmd' or ',.sh,.pl,.py') + + environ = kw.get('environ', getattr(self, 'environ', os.environ)) + + ret = '' + + filename = Utils.to_list(filename) + msg = kw.get('msg', ', '.join(filename)) + + var = kw.get('var', '') + if not var: + var = re.sub(r'[-.]', '_', filename[0].upper()) + + path_list = kw.get('path_list', '') + if path_list: + path_list = Utils.to_list(path_list) + else: + path_list = environ.get('PATH', '').split(os.pathsep) + + if kw.get('value'): + # user-provided in command-line options and passed to find_program + ret = self.cmd_to_list(kw['value']) + elif environ.get(var): + # user-provided in the os environment + ret = self.cmd_to_list(environ[var]) + elif self.env[var]: + # a default option in the wscript file + ret = self.cmd_to_list(self.env[var]) + else: + if not ret: + ret = self.find_binary(filename, exts.split(','), path_list) + 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) + ret = self.cmd_to_list(ret) + + if ret: + if len(ret) == 1: + retmsg = ret[0] + else: + retmsg = ret + else: + retmsg = False + + self.msg('Checking for program %r' % msg, retmsg, **kw) + if not kw.get('quiet'): + 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 %r' % filename) + + interpreter = kw.get('interpreter') + if interpreter is None: + if not Utils.check_exe(ret[0], env=environ): + self.fatal('Program %r is not executable' % ret) + self.env[var] = ret + else: + self.env[var] = self.env[interpreter] + ret + + return ret + +@conf +def find_binary(self, filenames, exts, paths): + for f in filenames: + for ext in exts: + exe_name = f + ext + if os.path.isabs(exe_name): + if os.path.isfile(exe_name): + return exe_name + else: + for path in paths: + x = os.path.expanduser(os.path.join(path, exe_name)) + if os.path.isfile(x): + return x + return None + +@conf +def run_build(self, *k, **kw): + """ + Create a temporary build context to execute a build. A reference to that build + context is kept on self.test_bld for debugging purposes, and you should not rely + on it too much (read the note on the cache below). + The parameters given in the arguments to this function are passed as arguments for + a single task generator created in the build. Only three parameters are obligatory: + + :param features: features to pass to a task generator created in the build + :type features: list of string + :param compile_filename: file to create for the compilation (default: *test.c*) + :type compile_filename: string + :param code: code to write in the filename to compile + :type code: string + + Though this function returns *0* by default, the build may set an attribute named *retval* on the + build context object to return a particular value. See :py:func:`waflib.Tools.c_config.test_exec_fun` for example. + + This function also provides a limited cache. To use it, provide the following option:: + + def options(opt): + opt.add_option('--confcache', dest='confcache', default=0, + action='count', help='Use a configuration cache') + + And execute the configuration with the following command-line:: + + $ waf configure --confcache + + """ + lst = [str(v) for (p, v) in kw.items() if p != 'env'] + h = Utils.h_list(lst) + dir = self.bldnode.abspath() + os.sep + (not Utils.is_win32 and '.' or '') + 'conf_check_' + Utils.to_hex(h) + + try: + os.makedirs(dir) + except OSError: + pass + + try: + os.stat(dir) + except OSError: + self.fatal('cannot use the configuration test folder %r' % dir) + + cachemode = getattr(Options.options, 'confcache', None) + if cachemode == 1: + try: + proj = ConfigSet.ConfigSet(os.path.join(dir, 'cache_run_build')) + except EnvironmentError: + pass + else: + ret = proj['cache_run_build'] + if isinstance(ret, str) and ret.startswith('Test does not build'): + self.fatal(ret) + return ret + + bdir = os.path.join(dir, 'testbuild') + + if not os.path.exists(bdir): + os.makedirs(bdir) + + cls_name = kw.get('run_build_cls') or getattr(self, 'run_build_cls', 'build') + self.test_bld = bld = Context.create_context(cls_name, top_dir=dir, out_dir=bdir) + bld.init_dirs() + bld.progress_bar = 0 + bld.targets = '*' + + bld.logger = self.logger + bld.all_envs.update(self.all_envs) # not really necessary + bld.env = kw['env'] + + bld.kw = kw + bld.conf = self + kw['build_fun'](bld) + ret = -1 + try: + try: + bld.compile() + except Errors.WafError: + ret = 'Test does not build: %s' % traceback.format_exc() + self.fatal(ret) + else: + ret = getattr(bld, 'retval', 0) + finally: + if cachemode == 1: + # cache the results each time + proj = ConfigSet.ConfigSet() + proj['cache_run_build'] = ret + proj.store(os.path.join(dir, 'cache_run_build')) + else: + shutil.rmtree(dir) + return ret + +@conf +def ret_msg(self, msg, args): + if isinstance(msg, str): + return msg + return msg(args) + +@conf +def test(self, *k, **kw): + + if not 'env' in kw: + kw['env'] = self.env.derive() + + # validate_c for example + if kw.get('validate'): + kw['validate'](kw) + + self.start_msg(kw['msg'], **kw) + ret = None + try: + ret = self.run_build(*k, **kw) + except self.errors.ConfigurationError: + self.end_msg(kw['errmsg'], 'YELLOW', **kw) + if Logs.verbose > 1: + raise + else: + self.fatal('The configuration failed') + else: + kw['success'] = ret + + if kw.get('post_check'): + ret = kw['post_check'](kw) + + if ret: + self.end_msg(kw['errmsg'], 'YELLOW', **kw) + self.fatal('The configuration failed %r' % ret) + else: + self.end_msg(self.ret_msg(kw['okmsg'], kw), **kw) + return ret + |