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 reference to that build
512	context is kept on self.test_bld for debugging purposes, and you should not rely
513	on it too much (read the note on the cache below).
514	The parameters given in the arguments to this function are passed as arguments for
515	a single task generator created in the build. Only three parameters are obligatory:
516
517	:param features: features to pass to a task generator created in the build
518	:type features: list of string
519	:param compile_filename: file to create for the compilation (default: *test.c*)
520	:type compile_filename: string
521	:param code: code to write in the filename to compile
522	:type code: string
523
524	Though this function returns *0* by default, the build may set an attribute named *retval* on the
525	build context object to return a particular value. See :py:func:`waflib.Tools.c_config.test_exec_fun` for example.
526
527	This function also features a cache which can be enabled by the following option::
528
529		def options(opt):
530			opt.add_option('--confcache', dest='confcache', default=0,
531				action='count', help='Use a configuration cache')
532
533	And execute the configuration with the following command-line::
534
535		$ waf configure --confcache
536
537	"""
538	buf = []
539	for key in sorted(kw.keys()):
540		v = kw[key]
541		if hasattr(v, '__call__'):
542			buf.append(Utils.h_fun(v))
543		else:
544			buf.append(str(v))
545	h = Utils.h_list(buf)
546	dir = self.bldnode.abspath() + os.sep + (not Utils.is_win32 and '.' or '') + 'conf_check_' + Utils.to_hex(h)
547
548	cachemode = kw.get('confcache', getattr(Options.options, 'confcache', None))
549
550	if not cachemode and os.path.exists(dir):
551		shutil.rmtree(dir)
552
553	try:
554		os.makedirs(dir)
555	except OSError:
556		pass
557
558	try:
559		os.stat(dir)
560	except OSError:
561		self.fatal('cannot use the configuration test folder %r' % dir)
562
563	if cachemode == 1:
564		try:
565			proj = ConfigSet.ConfigSet(os.path.join(dir, 'cache_run_build'))
566		except EnvironmentError:
567			pass
568		else:
569			ret = proj['cache_run_build']
570			if isinstance(ret, str) and ret.startswith('Test does not build'):
571				self.fatal(ret)
572			return ret
573
574	bdir = os.path.join(dir, 'testbuild')
575
576	if not os.path.exists(bdir):
577		os.makedirs(bdir)
578
579	cls_name = kw.get('run_build_cls') or getattr(self, 'run_build_cls', 'build')
580	self.test_bld = bld = Context.create_context(cls_name, top_dir=dir, out_dir=bdir)
581	bld.init_dirs()
582	bld.progress_bar = 0
583	bld.targets = '*'
584
585	bld.logger = self.logger
586	bld.all_envs.update(self.all_envs) # not really necessary
587	bld.env = kw['env']
588
589	bld.kw = kw
590	bld.conf = self
591	kw['build_fun'](bld)
592	ret = -1
593	try:
594		try:
595			bld.compile()
596		except Errors.WafError:
597			ret = 'Test does not build: %s' % traceback.format_exc()
598			self.fatal(ret)
599		else:
600			ret = getattr(bld, 'retval', 0)
601	finally:
602		if cachemode:
603			# cache the results each time
604			proj = ConfigSet.ConfigSet()
605			proj['cache_run_build'] = ret
606			proj.store(os.path.join(dir, 'cache_run_build'))
607		else:
608			shutil.rmtree(dir)
609	return ret
610
611@conf
612def ret_msg(self, msg, args):
613	if isinstance(msg, str):
614		return msg
615	return msg(args)
616
617@conf
618def test(self, *k, **kw):
619
620	if not 'env' in kw:
621		kw['env'] = self.env.derive()
622
623	# validate_c for example
624	if kw.get('validate'):
625		kw['validate'](kw)
626
627	self.start_msg(kw['msg'], **kw)
628	ret = None
629	try:
630		ret = self.run_build(*k, **kw)
631	except self.errors.ConfigurationError:
632		self.end_msg(kw['errmsg'], 'YELLOW', **kw)
633		if Logs.verbose > 1:
634			raise
635		else:
636			self.fatal('The configuration failed')
637	else:
638		kw['success'] = ret
639
640	if kw.get('post_check'):
641		ret = kw['post_check'](kw)
642
643	if ret:
644		self.end_msg(kw['errmsg'], 'YELLOW', **kw)
645		self.fatal('The configuration failed %r' % ret)
646	else:
647		self.end_msg(self.ret_msg(kw['okmsg'], kw), **kw)
648	return ret
649
650