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			conf.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
184		if not (self.env.NO_LOCK_IN_RUN or env.environ.get('NO_LOCK_IN_RUN') or getattr(Options.options, 'no_lock_in_run')):
185			env.store(os.path.join(Context.run_dir, Options.lockfile))
186		if not (self.env.NO_LOCK_IN_TOP or env.environ.get('NO_LOCK_IN_TOP') or getattr(Options.options, 'no_lock_in_top')):
187			env.store(os.path.join(Context.top_dir, Options.lockfile))
188		if not (self.env.NO_LOCK_IN_OUT or env.environ.get('NO_LOCK_IN_OUT') or getattr(Options.options, 'no_lock_in_out')):
189			env.store(os.path.join(Context.out_dir, Options.lockfile))
190
191	def prepare_env(self, env):
192		"""
193		Insert *PREFIX*, *BINDIR* and *LIBDIR* values into ``env``
194
195		:type env: :py:class:`waflib.ConfigSet.ConfigSet`
196		:param env: a ConfigSet, usually ``conf.env``
197		"""
198		if not env.PREFIX:
199			if Options.options.prefix or Utils.is_win32:
200				env.PREFIX = Options.options.prefix
201			else:
202				env.PREFIX = '/'
203		if not env.BINDIR:
204			if Options.options.bindir:
205				env.BINDIR = Options.options.bindir
206			else:
207				env.BINDIR = Utils.subst_vars('${PREFIX}/bin', env)
208		if not env.LIBDIR:
209			if Options.options.libdir:
210				env.LIBDIR = Options.options.libdir
211			else:
212				env.LIBDIR = Utils.subst_vars('${PREFIX}/lib%s' % Utils.lib64(), env)
213
214	def store(self):
215		"""Save the config results into the cache file"""
216		n = self.cachedir.make_node('build.config.py')
217		n.write('version = 0x%x\ntools = %r\n' % (Context.HEXVERSION, self.tools))
218
219		if not self.all_envs:
220			self.fatal('nothing to store in the configuration context!')
221
222		for key in self.all_envs:
223			tmpenv = self.all_envs[key]
224			tmpenv.store(os.path.join(self.cachedir.abspath(), key + Build.CACHE_SUFFIX))
225
226	def load(self, tool_list, tooldir=None, funs=None, with_sys_path=True, cache=False):
227		"""
228		Load Waf tools, which will be imported whenever a build is started.
229
230		:param tool_list: waf tools to import
231		:type tool_list: list of string
232		:param tooldir: paths for the imports
233		:type tooldir: list of string
234		:param funs: functions to execute from the waf tools
235		:type funs: list of string
236		:param cache: whether to prevent the tool from running twice
237		:type cache: bool
238		"""
239
240		tools = Utils.to_list(tool_list)
241		if tooldir:
242			tooldir = Utils.to_list(tooldir)
243		for tool in tools:
244			# avoid loading the same tool more than once with the same functions
245			# used by composite projects
246
247			if cache:
248				mag = (tool, id(self.env), tooldir, funs)
249				if mag in self.tool_cache:
250					self.to_log('(tool %s is already loaded, skipping)' % tool)
251					continue
252				self.tool_cache.append(mag)
253
254			module = None
255			try:
256				module = Context.load_tool(tool, tooldir, ctx=self, with_sys_path=with_sys_path)
257			except ImportError as e:
258				self.fatal('Could not load the Waf tool %r from %r\n%s' % (tool, getattr(e, 'waf_sys_path', sys.path), e))
259			except Exception as e:
260				self.to_log('imp %r (%r & %r)' % (tool, tooldir, funs))
261				self.to_log(traceback.format_exc())
262				raise
263
264			if funs is not None:
265				self.eval_rules(funs)
266			else:
267				func = getattr(module, 'configure', None)
268				if func:
269					if type(func) is type(Utils.readf):
270						func(self)
271					else:
272						self.eval_rules(func)
273
274			self.tools.append({'tool':tool, 'tooldir':tooldir, 'funs':funs})
275
276	def post_recurse(self, node):
277		"""
278		Records the path and a hash of the scripts visited, see :py:meth:`waflib.Context.Context.post_recurse`
279
280		:param node: script
281		:type node: :py:class:`waflib.Node.Node`
282		"""
283		super(ConfigurationContext, self).post_recurse(node)
284		self.hash = Utils.h_list((self.hash, node.read('rb')))
285		self.files.append(node.abspath())
286
287	def eval_rules(self, rules):
288		"""
289		Execute configuration tests provided as list of functions to run
290
291		:param rules: list of configuration method names
292		:type rules: list of string
293		"""
294		self.rules = Utils.to_list(rules)
295		for x in self.rules:
296			f = getattr(self, x)
297			if not f:
298				self.fatal('No such configuration function %r' % x)
299			f()
300
301def conf(f):
302	"""
303	Decorator: attach new configuration functions to :py:class:`waflib.Build.BuildContext` and
304	:py:class:`waflib.Configure.ConfigurationContext`. The methods bound will accept a parameter
305	named 'mandatory' to disable the configuration errors::
306
307		def configure(conf):
308			conf.find_program('abc', mandatory=False)
309
310	:param f: method to bind
311	:type f: function
312	"""
313	def fun(*k, **kw):
314		mandatory = kw.pop('mandatory', True)
315		try:
316			return f(*k, **kw)
317		except Errors.ConfigurationError:
318			if mandatory:
319				raise
320
321	fun.__name__ = f.__name__
322	setattr(ConfigurationContext, f.__name__, fun)
323	setattr(Build.BuildContext, f.__name__, fun)
324	return f
325
326@conf
327def add_os_flags(self, var, dest=None, dup=False):
328	"""
329	Import operating system environment values into ``conf.env`` dict::
330
331		def configure(conf):
332			conf.add_os_flags('CFLAGS')
333
334	:param var: variable to use
335	:type var: string
336	:param dest: destination variable, by default the same as var
337	:type dest: string
338	:param dup: add the same set of flags again
339	:type dup: bool
340	"""
341	try:
342		flags = shlex.split(self.environ[var])
343	except KeyError:
344		return
345	if dup or ''.join(flags) not in ''.join(Utils.to_list(self.env[dest or var])):
346		self.env.append_value(dest or var, flags)
347
348@conf
349def cmd_to_list(self, cmd):
350	"""
351	Detect if a command is written in pseudo shell like ``ccache g++`` and return a list.
352
353	:param cmd: command
354	:type cmd: a string or a list of string
355	"""
356	if isinstance(cmd, str):
357		if os.path.isfile(cmd):
358			# do not take any risk
359			return [cmd]
360		if os.sep == '/':
361			return shlex.split(cmd)
362		else:
363			try:
364				return shlex.split(cmd, posix=False)
365			except TypeError:
366				# Python 2.5 on windows?
367				return shlex.split(cmd)
368	return cmd
369
370@conf
371def check_waf_version(self, mini='1.9.99', maxi='2.1.0', **kw):
372	"""
373	Raise a Configuration error if the Waf version does not strictly match the given bounds::
374
375		conf.check_waf_version(mini='1.9.99', maxi='2.1.0')
376
377	:type  mini: number, tuple or string
378	:param mini: Minimum required version
379	:type  maxi: number, tuple or string
380	:param maxi: Maximum allowed version
381	"""
382	self.start_msg('Checking for waf version in %s-%s' % (str(mini), str(maxi)), **kw)
383	ver = Context.HEXVERSION
384	if Utils.num2ver(mini) > ver:
385		self.fatal('waf version should be at least %r (%r found)' % (Utils.num2ver(mini), ver))
386	if Utils.num2ver(maxi) < ver:
387		self.fatal('waf version should be at most %r (%r found)' % (Utils.num2ver(maxi), ver))
388	self.end_msg('ok', **kw)
389
390@conf
391def find_file(self, filename, path_list=[]):
392	"""
393	Find a file in a list of paths
394
395	:param filename: name of the file to search for
396	:param path_list: list of directories to search
397	:return: the first matching filename; else a configuration exception is raised
398	"""
399	for n in Utils.to_list(filename):
400		for d in Utils.to_list(path_list):
401			p = os.path.expanduser(os.path.join(d, n))
402			if os.path.exists(p):
403				return p
404	self.fatal('Could not find %r' % filename)
405
406@conf
407def find_program(self, filename, **kw):
408	"""
409	Search for a program on the operating system
410
411	When var is used, you may set os.environ[var] to help find a specific program version, for example::
412
413		$ CC='ccache gcc' waf configure
414
415	:param path_list: paths to use for searching
416	:type param_list: list of string
417	: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
418	:type var: string
419	:param value: obtain the program from the value passed exclusively
420	:type value: list or string (list is preferred)
421	:param exts: list of extensions for the binary (do not add an extension for portability)
422	:type exts: list of string
423	:param msg: name to display in the log, by default filename is used
424	:type msg: string
425	:param interpreter: interpreter for the program
426	:type interpreter: ConfigSet variable key
427	:raises: :py:class:`waflib.Errors.ConfigurationError`
428	"""
429
430	exts = kw.get('exts', Utils.is_win32 and '.exe,.com,.bat,.cmd' or ',.sh,.pl,.py')
431
432	environ = kw.get('environ', getattr(self, 'environ', os.environ))
433
434	ret = ''
435
436	filename = Utils.to_list(filename)
437	msg = kw.get('msg', ', '.join(filename))
438
439	var = kw.get('var', '')
440	if not var:
441		var = re.sub(r'[-.]', '_', filename[0].upper())
442
443	path_list = kw.get('path_list', '')
444	if path_list:
445		path_list = Utils.to_list(path_list)
446	else:
447		path_list = environ.get('PATH', '').split(os.pathsep)
448
449	if kw.get('value'):
450		# user-provided in command-line options and passed to find_program
451		ret = self.cmd_to_list(kw['value'])
452	elif environ.get(var):
453		# user-provided in the os environment
454		ret = self.cmd_to_list(environ[var])
455	elif self.env[var]:
456		# a default option in the wscript file
457		ret = self.cmd_to_list(self.env[var])
458	else:
459		if not ret:
460			ret = self.find_binary(filename, exts.split(','), path_list)
461		if not ret and Utils.winreg:
462			ret = Utils.get_registry_app_path(Utils.winreg.HKEY_CURRENT_USER, filename)
463		if not ret and Utils.winreg:
464			ret = Utils.get_registry_app_path(Utils.winreg.HKEY_LOCAL_MACHINE, filename)
465		ret = self.cmd_to_list(ret)
466
467	if ret:
468		if len(ret) == 1:
469			retmsg = ret[0]
470		else:
471			retmsg = ret
472	else:
473		retmsg = False
474
475	self.msg('Checking for program %r' % msg, retmsg, **kw)
476	if not kw.get('quiet'):
477		self.to_log('find program=%r paths=%r var=%r -> %r' % (filename, path_list, var, ret))
478
479	if not ret:
480		self.fatal(kw.get('errmsg', '') or 'Could not find the program %r' % filename)
481
482	interpreter = kw.get('interpreter')
483	if interpreter is None:
484		if not Utils.check_exe(ret[0], env=environ):
485			self.fatal('Program %r is not executable' % ret)
486		self.env[var] = ret
487	else:
488		self.env[var] = self.env[interpreter] + ret
489
490	return ret
491
492@conf
493def find_binary(self, filenames, exts, paths):
494	for f in filenames:
495		for ext in exts:
496			exe_name = f + ext
497			if os.path.isabs(exe_name):
498				if os.path.isfile(exe_name):
499					return exe_name
500			else:
501				for path in paths:
502					x = os.path.expanduser(os.path.join(path, exe_name))
503					if os.path.isfile(x):
504						return x
505	return None
506
507@conf
508def run_build(self, *k, **kw):
509	"""
510	Create a temporary build context to execute a build. A reference to that build
511	context is kept on self.test_bld for debugging purposes, and you should not rely
512	on it too much (read the note on the cache below).
513	The parameters given in the arguments to this function are passed as arguments for
514	a single task generator created in the build. Only three parameters are obligatory:
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: code to write in the filename to compile
521	:type code: string
522
523	Though this function returns *0* by default, the build may set an 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	This function also provides a limited cache. To use it, provide the following option::
527
528		def options(opt):
529			opt.add_option('--confcache', dest='confcache', default=0,
530				action='count', help='Use a configuration cache')
531
532	And execute the configuration with the following command-line::
533
534		$ waf configure --confcache
535
536	"""
537	lst = [str(v) for (p, v) in kw.items() if p != 'env']
538	h = Utils.h_list(lst)
539	dir = self.bldnode.abspath() + os.sep + (not Utils.is_win32 and '.' or '') + 'conf_check_' + Utils.to_hex(h)
540
541	try:
542		os.makedirs(dir)
543	except OSError:
544		pass
545
546	try:
547		os.stat(dir)
548	except OSError:
549		self.fatal('cannot use the configuration test folder %r' % dir)
550
551	cachemode = getattr(Options.options, 'confcache', None)
552	if cachemode == 1:
553		try:
554			proj = ConfigSet.ConfigSet(os.path.join(dir, 'cache_run_build'))
555		except EnvironmentError:
556			pass
557		else:
558			ret = proj['cache_run_build']
559			if isinstance(ret, str) and ret.startswith('Test does not build'):
560				self.fatal(ret)
561			return ret
562
563	bdir = os.path.join(dir, 'testbuild')
564
565	if not os.path.exists(bdir):
566		os.makedirs(bdir)
567
568	cls_name = kw.get('run_build_cls') or getattr(self, 'run_build_cls', 'build')
569	self.test_bld = bld = Context.create_context(cls_name, top_dir=dir, out_dir=bdir)
570	bld.init_dirs()
571	bld.progress_bar = 0
572	bld.targets = '*'
573
574	bld.logger = self.logger
575	bld.all_envs.update(self.all_envs) # not really necessary
576	bld.env = kw['env']
577
578	bld.kw = kw
579	bld.conf = self
580	kw['build_fun'](bld)
581	ret = -1
582	try:
583		try:
584			bld.compile()
585		except Errors.WafError:
586			ret = 'Test does not build: %s' % traceback.format_exc()
587			self.fatal(ret)
588		else:
589			ret = getattr(bld, 'retval', 0)
590	finally:
591		if cachemode == 1:
592			# cache the results each time
593			proj = ConfigSet.ConfigSet()
594			proj['cache_run_build'] = ret
595			proj.store(os.path.join(dir, 'cache_run_build'))
596		else:
597			shutil.rmtree(dir)
598	return ret
599
600@conf
601def ret_msg(self, msg, args):
602	if isinstance(msg, str):
603		return msg
604	return msg(args)
605
606@conf
607def test(self, *k, **kw):
608
609	if not 'env' in kw:
610		kw['env'] = self.env.derive()
611
612	# validate_c for example
613	if kw.get('validate'):
614		kw['validate'](kw)
615
616	self.start_msg(kw['msg'], **kw)
617	ret = None
618	try:
619		ret = self.run_build(*k, **kw)
620	except self.errors.ConfigurationError:
621		self.end_msg(kw['errmsg'], 'YELLOW', **kw)
622		if Logs.verbose > 1:
623			raise
624		else:
625			self.fatal('The configuration failed')
626	else:
627		kw['success'] = ret
628
629	if kw.get('post_check'):
630		ret = kw['post_check'](kw)
631
632	if ret:
633		self.end_msg(kw['errmsg'], 'YELLOW', **kw)
634		self.fatal('The configuration failed %r' % ret)
635	else:
636		self.end_msg(self.ret_msg(kw['okmsg'], kw), **kw)
637	return ret
638
639