1#!/usr/bin/env python 2# encoding: utf-8 3# Thomas Nagy, 2005-2018 (ita) 4 5""" 6Configuration system 7 8A :py:class:`waflib.Configure.ConfigurationContext` instance is created when ``waf configure`` is called, it is used to: 9 10* create data dictionaries (ConfigSet instances) 11* store the list of modules to import 12* hold configuration routines such as ``find_program``, etc 13""" 14 15import os, re, shlex, shutil, sys, time, traceback 16from waflib import ConfigSet, Utils, Options, Logs, Context, Build, Errors 17 18WAF_CONFIG_LOG = 'config.log' 19"""Name of the configuration log file""" 20 21autoconfig = False 22"""Execute the configuration automatically""" 23 24conf_template = '''# project %(app)s configured on %(now)s by 25# waf %(wafver)s (abi %(abi)s, python %(pyver)x on %(systype)s) 26# using %(args)s 27#''' 28 29class ConfigurationContext(Context.Context): 30 '''configures the project''' 31 32 cmd = 'configure' 33 34 error_handlers = [] 35 """ 36 Additional functions to handle configuration errors 37 """ 38 39 def __init__(self, **kw): 40 super(ConfigurationContext, self).__init__(**kw) 41 self.environ = dict(os.environ) 42 self.all_envs = {} 43 44 self.top_dir = None 45 self.out_dir = None 46 47 self.tools = [] # tools loaded in the configuration, and that will be loaded when building 48 49 self.hash = 0 50 self.files = [] 51 52 self.tool_cache = [] 53 54 self.setenv('') 55 56 def setenv(self, name, env=None): 57 """ 58 Set a new config set for conf.env. If a config set of that name already exists, 59 recall it without modification. 60 61 The name is the filename prefix to save to ``c4che/NAME_cache.py``, and it 62 is also used as *variants* by the build commands. 63 Though related to variants, whatever kind of data may be stored in the config set:: 64 65 def configure(cfg): 66 cfg.env.ONE = 1 67 cfg.setenv('foo') 68 cfg.env.ONE = 2 69 70 def build(bld): 71 2 == bld.env_of_name('foo').ONE 72 73 :param name: name of the configuration set 74 :type name: string 75 :param env: ConfigSet to copy, or an empty ConfigSet is created 76 :type env: :py:class:`waflib.ConfigSet.ConfigSet` 77 """ 78 if name not in self.all_envs or env: 79 if not env: 80 env = ConfigSet.ConfigSet() 81 self.prepare_env(env) 82 else: 83 env = env.derive() 84 self.all_envs[name] = env 85 self.variant = name 86 87 def get_env(self): 88 """Getter for the env property""" 89 return self.all_envs[self.variant] 90 def set_env(self, val): 91 """Setter for the env property""" 92 self.all_envs[self.variant] = val 93 94 env = property(get_env, set_env) 95 96 def init_dirs(self): 97 """ 98 Initialize the project directory and the build directory 99 """ 100 101 top = self.top_dir 102 if not top: 103 top = Options.options.top 104 if not top: 105 top = getattr(Context.g_module, Context.TOP, None) 106 if not top: 107 top = self.path.abspath() 108 top = os.path.abspath(top) 109 110 self.srcnode = (os.path.isabs(top) and self.root or self.path).find_dir(top) 111 assert(self.srcnode) 112 113 out = self.out_dir 114 if not out: 115 out = Options.options.out 116 if not out: 117 out = getattr(Context.g_module, Context.OUT, None) 118 if not out: 119 out = Options.lockfile.replace('.lock-waf_%s_' % sys.platform, '').replace('.lock-waf', '') 120 121 # someone can be messing with symlinks 122 out = os.path.realpath(out) 123 124 self.bldnode = (os.path.isabs(out) and self.root or self.path).make_node(out) 125 self.bldnode.mkdir() 126 127 if not os.path.isdir(self.bldnode.abspath()): 128 self.fatal('Could not create the build directory %s' % self.bldnode.abspath()) 129 130 def execute(self): 131 """ 132 See :py:func:`waflib.Context.Context.execute` 133 """ 134 self.init_dirs() 135 136 self.cachedir = self.bldnode.make_node(Build.CACHE_DIR) 137 self.cachedir.mkdir() 138 139 path = os.path.join(self.bldnode.abspath(), WAF_CONFIG_LOG) 140 self.logger = Logs.make_logger(path, 'cfg') 141 142 app = getattr(Context.g_module, 'APPNAME', '') 143 if app: 144 ver = getattr(Context.g_module, 'VERSION', '') 145 if ver: 146 app = "%s (%s)" % (app, ver) 147 148 params = {'now': time.ctime(), 'pyver': sys.hexversion, 'systype': sys.platform, 'args': " ".join(sys.argv), 'wafver': Context.WAFVERSION, 'abi': Context.ABI, 'app': app} 149 self.to_log(conf_template % params) 150 self.msg('Setting top to', self.srcnode.abspath()) 151 self.msg('Setting out to', self.bldnode.abspath()) 152 153 if id(self.srcnode) == id(self.bldnode): 154 Logs.warn('Setting top == out') 155 elif id(self.path) != id(self.srcnode): 156 if self.srcnode.is_child_of(self.path): 157 Logs.warn('Are you certain that you do not want to set top="." ?') 158 159 super(ConfigurationContext, self).execute() 160 161 self.store() 162 163 Context.top_dir = self.srcnode.abspath() 164 Context.out_dir = self.bldnode.abspath() 165 166 # this will write a configure lock so that subsequent builds will 167 # consider the current path as the root directory (see prepare_impl). 168 # to remove: use 'waf distclean' 169 env = ConfigSet.ConfigSet() 170 env.argv = sys.argv 171 env.options = Options.options.__dict__ 172 env.config_cmd = self.cmd 173 174 env.run_dir = Context.run_dir 175 env.top_dir = Context.top_dir 176 env.out_dir = Context.out_dir 177 178 # conf.hash & conf.files hold wscript files paths and hash 179 # (used only by Configure.autoconfig) 180 env.hash = self.hash 181 env.files = self.files 182 env.environ = dict(self.environ) 183 env.launch_dir = Context.launch_dir 184 185 if not (self.env.NO_LOCK_IN_RUN or env.environ.get('NO_LOCK_IN_RUN') or getattr(Options.options, 'no_lock_in_run')): 186 env.store(os.path.join(Context.run_dir, Options.lockfile)) 187 if not (self.env.NO_LOCK_IN_TOP or env.environ.get('NO_LOCK_IN_TOP') or getattr(Options.options, 'no_lock_in_top')): 188 env.store(os.path.join(Context.top_dir, Options.lockfile)) 189 if not (self.env.NO_LOCK_IN_OUT or env.environ.get('NO_LOCK_IN_OUT') or getattr(Options.options, 'no_lock_in_out')): 190 env.store(os.path.join(Context.out_dir, Options.lockfile)) 191 192 def prepare_env(self, env): 193 """ 194 Insert *PREFIX*, *BINDIR* and *LIBDIR* values into ``env`` 195 196 :type env: :py:class:`waflib.ConfigSet.ConfigSet` 197 :param env: a ConfigSet, usually ``conf.env`` 198 """ 199 if not env.PREFIX: 200 if Options.options.prefix or Utils.is_win32: 201 env.PREFIX = Options.options.prefix 202 else: 203 env.PREFIX = '/' 204 if not env.BINDIR: 205 if Options.options.bindir: 206 env.BINDIR = Options.options.bindir 207 else: 208 env.BINDIR = Utils.subst_vars('${PREFIX}/bin', env) 209 if not env.LIBDIR: 210 if Options.options.libdir: 211 env.LIBDIR = Options.options.libdir 212 else: 213 env.LIBDIR = Utils.subst_vars('${PREFIX}/lib%s' % Utils.lib64(), env) 214 215 def store(self): 216 """Save the config results into the cache file""" 217 n = self.cachedir.make_node('build.config.py') 218 n.write('version = 0x%x\ntools = %r\n' % (Context.HEXVERSION, self.tools)) 219 220 if not self.all_envs: 221 self.fatal('nothing to store in the configuration context!') 222 223 for key in self.all_envs: 224 tmpenv = self.all_envs[key] 225 tmpenv.store(os.path.join(self.cachedir.abspath(), key + Build.CACHE_SUFFIX)) 226 227 def load(self, tool_list, tooldir=None, funs=None, with_sys_path=True, cache=False): 228 """ 229 Load Waf tools, which will be imported whenever a build is started. 230 231 :param tool_list: waf tools to import 232 :type tool_list: list of string 233 :param tooldir: paths for the imports 234 :type tooldir: list of string 235 :param funs: functions to execute from the waf tools 236 :type funs: list of string 237 :param cache: whether to prevent the tool from running twice 238 :type cache: bool 239 """ 240 241 tools = Utils.to_list(tool_list) 242 if tooldir: 243 tooldir = Utils.to_list(tooldir) 244 for tool in tools: 245 # avoid loading the same tool more than once with the same functions 246 # used by composite projects 247 248 if cache: 249 mag = (tool, id(self.env), tooldir, funs) 250 if mag in self.tool_cache: 251 self.to_log('(tool %s is already loaded, skipping)' % tool) 252 continue 253 self.tool_cache.append(mag) 254 255 module = None 256 try: 257 module = Context.load_tool(tool, tooldir, ctx=self, with_sys_path=with_sys_path) 258 except ImportError as e: 259 self.fatal('Could not load the Waf tool %r from %r\n%s' % (tool, getattr(e, 'waf_sys_path', sys.path), e)) 260 except Exception as e: 261 self.to_log('imp %r (%r & %r)' % (tool, tooldir, funs)) 262 self.to_log(traceback.format_exc()) 263 raise 264 265 if funs is not None: 266 self.eval_rules(funs) 267 else: 268 func = getattr(module, 'configure', None) 269 if func: 270 if type(func) is type(Utils.readf): 271 func(self) 272 else: 273 self.eval_rules(func) 274 275 self.tools.append({'tool':tool, 'tooldir':tooldir, 'funs':funs}) 276 277 def post_recurse(self, node): 278 """ 279 Records the path and a hash of the scripts visited, see :py:meth:`waflib.Context.Context.post_recurse` 280 281 :param node: script 282 :type node: :py:class:`waflib.Node.Node` 283 """ 284 super(ConfigurationContext, self).post_recurse(node) 285 self.hash = Utils.h_list((self.hash, node.read('rb'))) 286 self.files.append(node.abspath()) 287 288 def eval_rules(self, rules): 289 """ 290 Execute configuration tests provided as list of functions to run 291 292 :param rules: list of configuration method names 293 :type rules: list of string 294 """ 295 self.rules = Utils.to_list(rules) 296 for x in self.rules: 297 f = getattr(self, x) 298 if not f: 299 self.fatal('No such configuration function %r' % x) 300 f() 301 302def conf(f): 303 """ 304 Decorator: attach new configuration functions to :py:class:`waflib.Build.BuildContext` and 305 :py:class:`waflib.Configure.ConfigurationContext`. The methods bound will accept a parameter 306 named 'mandatory' to disable the configuration errors:: 307 308 def configure(conf): 309 conf.find_program('abc', mandatory=False) 310 311 :param f: method to bind 312 :type f: function 313 """ 314 def fun(*k, **kw): 315 mandatory = kw.pop('mandatory', True) 316 try: 317 return f(*k, **kw) 318 except Errors.ConfigurationError: 319 if mandatory: 320 raise 321 322 fun.__name__ = f.__name__ 323 setattr(ConfigurationContext, f.__name__, fun) 324 setattr(Build.BuildContext, f.__name__, fun) 325 return f 326 327@conf 328def add_os_flags(self, var, dest=None, dup=False): 329 """ 330 Import operating system environment values into ``conf.env`` dict:: 331 332 def configure(conf): 333 conf.add_os_flags('CFLAGS') 334 335 :param var: variable to use 336 :type var: string 337 :param dest: destination variable, by default the same as var 338 :type dest: string 339 :param dup: add the same set of flags again 340 :type dup: bool 341 """ 342 try: 343 flags = shlex.split(self.environ[var]) 344 except KeyError: 345 return 346 if dup or ''.join(flags) not in ''.join(Utils.to_list(self.env[dest or var])): 347 self.env.append_value(dest or var, flags) 348 349@conf 350def cmd_to_list(self, cmd): 351 """ 352 Detect if a command is written in pseudo shell like ``ccache g++`` and return a list. 353 354 :param cmd: command 355 :type cmd: a string or a list of string 356 """ 357 if isinstance(cmd, str): 358 if os.path.isfile(cmd): 359 # do not take any risk 360 return [cmd] 361 if os.sep == '/': 362 return shlex.split(cmd) 363 else: 364 try: 365 return shlex.split(cmd, posix=False) 366 except TypeError: 367 # Python 2.5 on windows? 368 return shlex.split(cmd) 369 return cmd 370 371@conf 372def check_waf_version(self, mini='1.9.99', maxi='2.1.0', **kw): 373 """ 374 Raise a Configuration error if the Waf version does not strictly match the given bounds:: 375 376 conf.check_waf_version(mini='1.9.99', maxi='2.1.0') 377 378 :type mini: number, tuple or string 379 :param mini: Minimum required version 380 :type maxi: number, tuple or string 381 :param maxi: Maximum allowed version 382 """ 383 self.start_msg('Checking for waf version in %s-%s' % (str(mini), str(maxi)), **kw) 384 ver = Context.HEXVERSION 385 if Utils.num2ver(mini) > ver: 386 self.fatal('waf version should be at least %r (%r found)' % (Utils.num2ver(mini), ver)) 387 if Utils.num2ver(maxi) < ver: 388 self.fatal('waf version should be at most %r (%r found)' % (Utils.num2ver(maxi), ver)) 389 self.end_msg('ok', **kw) 390 391@conf 392def find_file(self, filename, path_list=[]): 393 """ 394 Find a file in a list of paths 395 396 :param filename: name of the file to search for 397 :param path_list: list of directories to search 398 :return: the first matching filename; else a configuration exception is raised 399 """ 400 for n in Utils.to_list(filename): 401 for d in Utils.to_list(path_list): 402 p = os.path.expanduser(os.path.join(d, n)) 403 if os.path.exists(p): 404 return p 405 self.fatal('Could not find %r' % filename) 406 407@conf 408def find_program(self, filename, **kw): 409 """ 410 Search for a program on the operating system 411 412 When var is used, you may set os.environ[var] to help find a specific program version, for example:: 413 414 $ CC='ccache gcc' waf configure 415 416 :param path_list: paths to use for searching 417 :type param_list: list of string 418 :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 419 :type var: string 420 :param value: obtain the program from the value passed exclusively 421 :type value: list or string (list is preferred) 422 :param exts: list of extensions for the binary (do not add an extension for portability) 423 :type exts: list of string 424 :param msg: name to display in the log, by default filename is used 425 :type msg: string 426 :param interpreter: interpreter for the program 427 :type interpreter: ConfigSet variable key 428 :raises: :py:class:`waflib.Errors.ConfigurationError` 429 """ 430 431 exts = kw.get('exts', Utils.is_win32 and '.exe,.com,.bat,.cmd' or ',.sh,.pl,.py') 432 433 environ = kw.get('environ', getattr(self, 'environ', os.environ)) 434 435 ret = '' 436 437 filename = Utils.to_list(filename) 438 msg = kw.get('msg', ', '.join(filename)) 439 440 var = kw.get('var', '') 441 if not var: 442 var = re.sub(r'[-.]', '_', filename[0].upper()) 443 444 path_list = kw.get('path_list', '') 445 if path_list: 446 path_list = Utils.to_list(path_list) 447 else: 448 path_list = environ.get('PATH', '').split(os.pathsep) 449 450 if kw.get('value'): 451 # user-provided in command-line options and passed to find_program 452 ret = self.cmd_to_list(kw['value']) 453 elif environ.get(var): 454 # user-provided in the os environment 455 ret = self.cmd_to_list(environ[var]) 456 elif self.env[var]: 457 # a default option in the wscript file 458 ret = self.cmd_to_list(self.env[var]) 459 else: 460 if not ret: 461 ret = self.find_binary(filename, exts.split(','), path_list) 462 if not ret and Utils.winreg: 463 ret = Utils.get_registry_app_path(Utils.winreg.HKEY_CURRENT_USER, filename) 464 if not ret and Utils.winreg: 465 ret = Utils.get_registry_app_path(Utils.winreg.HKEY_LOCAL_MACHINE, filename) 466 ret = self.cmd_to_list(ret) 467 468 if ret: 469 if len(ret) == 1: 470 retmsg = ret[0] 471 else: 472 retmsg = ret 473 else: 474 retmsg = False 475 476 self.msg('Checking for program %r' % msg, retmsg, **kw) 477 if not kw.get('quiet'): 478 self.to_log('find program=%r paths=%r var=%r -> %r' % (filename, path_list, var, ret)) 479 480 if not ret: 481 self.fatal(kw.get('errmsg', '') or 'Could not find the program %r' % filename) 482 483 interpreter = kw.get('interpreter') 484 if interpreter is None: 485 if not Utils.check_exe(ret[0], env=environ): 486 self.fatal('Program %r is not executable' % ret) 487 self.env[var] = ret 488 else: 489 self.env[var] = self.env[interpreter] + ret 490 491 return ret 492 493@conf 494def find_binary(self, filenames, exts, paths): 495 for f in filenames: 496 for ext in exts: 497 exe_name = f + ext 498 if os.path.isabs(exe_name): 499 if os.path.isfile(exe_name): 500 return exe_name 501 else: 502 for path in paths: 503 x = os.path.expanduser(os.path.join(path, exe_name)) 504 if os.path.isfile(x): 505 return x 506 return None 507 508@conf 509def run_build(self, *k, **kw): 510 """ 511 Create a temporary build context to execute a build. A temporary reference to that build 512 context is kept on self.test_bld for debugging purposes. 513 The arguments to this function are passed to a single task generator for that build. 514 Only three parameters are mandatory: 515 516 :param features: features to pass to a task generator created in the build 517 :type features: list of string 518 :param compile_filename: file to create for the compilation (default: *test.c*) 519 :type compile_filename: string 520 :param code: input file contents 521 :type code: string 522 523 Though this function returns *0* by default, the build may bind attribute named *retval* on the 524 build context object to return a particular value. See :py:func:`waflib.Tools.c_config.test_exec_fun` for example. 525 526 The temporary builds creates a temporary folder; the name of that folder is calculated 527 by hashing input arguments to this function, with the exception of :py:class:`waflib.ConfigSet.ConfigSet` 528 objects which are used for both reading and writing values. 529 530 This function also features a cache which is disabled by default; that cache relies 531 on the hash value calculated as indicated above:: 532 533 def options(opt): 534 opt.add_option('--confcache', dest='confcache', default=0, 535 action='count', help='Use a configuration cache') 536 537 And execute the configuration with the following command-line:: 538 539 $ waf configure --confcache 540 541 """ 542 buf = [] 543 for key in sorted(kw.keys()): 544 v = kw[key] 545 if isinstance(v, ConfigSet.ConfigSet): 546 # values are being written to, so they are excluded from contributing to the hash 547 continue 548 elif hasattr(v, '__call__'): 549 buf.append(Utils.h_fun(v)) 550 else: 551 buf.append(str(v)) 552 h = Utils.h_list(buf) 553 dir = self.bldnode.abspath() + os.sep + (not Utils.is_win32 and '.' or '') + 'conf_check_' + Utils.to_hex(h) 554 555 cachemode = kw.get('confcache', getattr(Options.options, 'confcache', None)) 556 557 if not cachemode and os.path.exists(dir): 558 shutil.rmtree(dir) 559 560 try: 561 os.makedirs(dir) 562 except OSError: 563 pass 564 565 try: 566 os.stat(dir) 567 except OSError: 568 self.fatal('cannot use the configuration test folder %r' % dir) 569 570 if cachemode == 1: 571 try: 572 proj = ConfigSet.ConfigSet(os.path.join(dir, 'cache_run_build')) 573 except EnvironmentError: 574 pass 575 else: 576 ret = proj['cache_run_build'] 577 if isinstance(ret, str) and ret.startswith('Test does not build'): 578 self.fatal(ret) 579 return ret 580 581 bdir = os.path.join(dir, 'testbuild') 582 583 if not os.path.exists(bdir): 584 os.makedirs(bdir) 585 586 cls_name = kw.get('run_build_cls') or getattr(self, 'run_build_cls', 'build') 587 self.test_bld = bld = Context.create_context(cls_name, top_dir=dir, out_dir=bdir) 588 bld.init_dirs() 589 bld.progress_bar = 0 590 bld.targets = '*' 591 592 bld.logger = self.logger 593 bld.all_envs.update(self.all_envs) # not really necessary 594 bld.env = kw['env'] 595 596 bld.kw = kw 597 bld.conf = self 598 kw['build_fun'](bld) 599 ret = -1 600 try: 601 try: 602 bld.compile() 603 except Errors.WafError: 604 ret = 'Test does not build: %s' % traceback.format_exc() 605 self.fatal(ret) 606 else: 607 ret = getattr(bld, 'retval', 0) 608 finally: 609 if cachemode: 610 # cache the results each time 611 proj = ConfigSet.ConfigSet() 612 proj['cache_run_build'] = ret 613 proj.store(os.path.join(dir, 'cache_run_build')) 614 else: 615 shutil.rmtree(dir) 616 return ret 617 618@conf 619def ret_msg(self, msg, args): 620 if isinstance(msg, str): 621 return msg 622 return msg(args) 623 624@conf 625def test(self, *k, **kw): 626 627 if not 'env' in kw: 628 kw['env'] = self.env.derive() 629 630 # validate_c for example 631 if kw.get('validate'): 632 kw['validate'](kw) 633 634 self.start_msg(kw['msg'], **kw) 635 ret = None 636 try: 637 ret = self.run_build(*k, **kw) 638 except self.errors.ConfigurationError: 639 self.end_msg(kw['errmsg'], 'YELLOW', **kw) 640 if Logs.verbose > 1: 641 raise 642 else: 643 self.fatal('The configuration failed') 644 else: 645 kw['success'] = ret 646 647 if kw.get('post_check'): 648 ret = kw['post_check'](kw) 649 650 if ret: 651 self.end_msg(kw['errmsg'], 'YELLOW', **kw) 652 self.fatal('The configuration failed %r' % ret) 653 else: 654 self.end_msg(self.ret_msg(kw['okmsg'], kw), **kw) 655 return ret 656 657