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