Commits

Marianne Gagnon committed cbef4eb Merge

Merge ecsv's changes

  • Participants
  • Parent commits 31df17f, 0d17c12

Comments (0)

Files changed (78)

File waf

File contents unchanged.

File waflib/Build.py

+#!/usr/bin/env python
+# encoding: utf-8
+# Thomas Nagy, 2005-2010 (ita)
+
+"""
+Classes related to the build phase (build, clean, install, step, etc)
+
+The inheritance tree is the following:
+
+"""
+
+import os, sys, errno, re, datetime, shutil
+try: import cPickle
+except: import pickle as cPickle
+from waflib import Runner, TaskGen, Utils, ConfigSet, Task, Logs, Options, Context, Errors
+import waflib.Node
+
+CACHE_DIR = 'c4che'
+"""Location of the cache files"""
+
+CACHE_SUFFIX = '_cache.py'
+"""Suffix for the cache files"""
+
+INSTALL = 1337
+"""Positive value '->' install, see :py:attr:`waflib.Build.BuildContext.is_install`"""
+
+UNINSTALL = -1337
+"""Negative value '<-' uninstall, see :py:attr:`waflib.Build.BuildContext.is_install`"""
+
+SAVED_ATTRS = 'root node_deps raw_deps task_sigs'.split()
+"""Build class members to save between the runs (root, node_deps, raw_deps, task_sigs)"""
+
+CFG_FILES = 'cfg_files'
+"""Files from the build directory to hash before starting the build (``config.h`` written during the configuration)"""
+
+POST_AT_ONCE = 0
+"""Post mode: all task generators are posted before the build really starts"""
+
+POST_LAZY = 1
+"""Post mode: post the task generators group after group"""
+
+POST_BOTH = 2
+"""Post mode: post the task generators at once, then re-check them for each group"""
+
+class BuildContext(Context.Context):
+	'''executes the build'''
+
+	cmd = 'build'
+	variant = ''
+
+	def __init__(self, **kw):
+		super(BuildContext, self).__init__(**kw)
+
+		self.is_install = 0
+		"""Non-zero value when installing or uninstalling file"""
+
+		self.top_dir = kw.get('top_dir', Context.top_dir)
+
+		self.run_dir = kw.get('run_dir', Context.run_dir)
+
+		self.post_mode = POST_AT_ONCE
+		"""post the task generators at once, group-by-group, or both"""
+
+		# output directory - may be set until the nodes are considered
+		self.out_dir = kw.get('out_dir', Context.out_dir)
+
+		self.cache_dir = kw.get('cache_dir', None)
+		if not self.cache_dir:
+			self.cache_dir = self.out_dir + os.sep + CACHE_DIR
+
+		# map names to environments, the '' must be defined
+		self.all_envs = {}
+
+		# ======================================= #
+		# cache variables
+
+		self.task_sigs = {}
+		"""Signatures of the tasks (persists between build executions)"""
+
+		self.node_deps = {}
+		"""Dict of node dependencies found by :py:meth:`waflib.Task.Task.scan` (persists between build executions)"""
+
+		self.raw_deps = {}
+		"""Dict of custom data returned by :py:meth:`waflib.Task.Task.scan` (persists between build executions)"""
+
+		# list of folders that are already scanned
+		# so that we do not need to stat them one more time
+		self.cache_dir_contents = {}
+
+		self.task_gen_cache_names = {}
+
+		self.launch_dir = Context.launch_dir
+
+		self.jobs = Options.options.jobs
+		self.targets = Options.options.targets
+		self.keep = Options.options.keep
+		self.cache_global = Options.cache_global
+		self.nocache = Options.options.nocache
+		self.progress_bar = Options.options.progress_bar
+
+		############ stuff below has not been reviewed
+
+		# Manual dependencies.
+		self.deps_man = Utils.defaultdict(list)
+		"""Manual dependencies set by :py:meth:`waflib.Build.BuildContext.add_manual_dependency`"""
+
+		# just the structure here
+		self.current_group = 0
+		"""
+		Current build group
+		"""
+
+		self.groups = []
+		"""
+		List containing lists of task generators
+		"""
+		self.group_names = {}
+		"""
+		Map group names to the group lists. See :py:meth:`waflib.Build.BuildContext.add_group`
+		"""
+
+	def get_variant_dir(self):
+		"""Getter for the variant_dir attribute"""
+		if not self.variant:
+			return self.out_dir
+		return os.path.join(self.out_dir, self.variant)
+	variant_dir = property(get_variant_dir, None)
+
+	def __call__(self, *k, **kw):
+		"""
+		Create a task generator and add it to the current build group. The following forms are equivalent::
+
+			def build(bld):
+				tg = bld(a=1, b=2)
+
+			def build(bld):
+				tg = bld()
+				tg.a = 1
+				tg.b = 2
+
+			def build(bld):
+				tg = TaskGen.task_gen(a=1, b=2)
+				bld.add_to_group(tg, None)
+
+		:param group: group name to add the task generator to
+		:type group: string
+		"""
+		kw['bld'] = self
+		ret = TaskGen.task_gen(*k, **kw)
+		self.task_gen_cache_names = {} # reset the cache, each time
+		self.add_to_group(ret, group=kw.get('group', None))
+		return ret
+
+	def __copy__(self):
+		"""Implemented to prevents copies of build contexts (raises an exception)"""
+		raise Errors.WafError('build contexts are not supposed to be copied')
+
+	def install_files(self, *k, **kw):
+		"""Actual implementation provided by :py:meth:`waflib.Build.InstallContext.install_files`"""
+		pass
+
+	def install_as(self, *k, **kw):
+		"""Actual implementation provided by :py:meth:`waflib.Build.InstallContext.install_as`"""
+		pass
+
+	def symlink_as(self, *k, **kw):
+		"""Actual implementation provided by :py:meth:`waflib.Build.InstallContext.symlink_as`"""
+		pass
+
+	def load_envs(self):
+		"""
+		The configuration command creates files of the form ``build/c4che/NAMEcache.py``. This method
+		creates a :py:class:`waflib.ConfigSet.ConfigSet` instance for each ``NAME`` by reading those
+		files. The config sets are then stored in the dict :py:attr:`waflib.Build.BuildContext.allenvs`.
+		"""
+		try:
+			lst = Utils.listdir(self.cache_dir)
+		except OSError as e:
+			if e.errno == errno.ENOENT:
+				raise Errors.WafError('The project was not configured: run "waf configure" first!')
+			else:
+				raise
+
+		if not lst:
+			raise Errors.WafError('The cache directory is empty: reconfigure the project')
+
+		for fname in lst:
+			if fname.endswith(CACHE_SUFFIX):
+				env = ConfigSet.ConfigSet(os.path.join(self.cache_dir, fname))
+				name = fname[:-len(CACHE_SUFFIX)]
+				self.all_envs[name] = env
+
+				for f in env[CFG_FILES]:
+					newnode = self.root.find_resource(f)
+					try:
+						h = Utils.h_file(newnode.abspath())
+					except (IOError, AttributeError):
+						Logs.error('cannot find %r' % f)
+						h = Utils.SIG_NIL
+					newnode.sig = h
+
+	def init_dirs(self):
+		"""
+		Initialize the project directory and the build directory by creating the nodes
+		:py:attr:`waflib.Build.BuildContext.srcnode` and :py:attr:`waflib.Build.BuildContext.bldnode`
+		corresponding to ``top_dir`` and ``variant_dir`` respectively. The ``bldnode`` directory will be
+		created if it does not exist.
+		"""
+
+		if not (os.path.isabs(self.top_dir) and os.path.isabs(self.out_dir)):
+			raise Errors.WafError('The project was not configured: run "waf configure" first!')
+
+		self.path = self.srcnode = self.root.find_dir(self.top_dir)
+		self.bldnode = self.root.make_node(self.variant_dir)
+		self.bldnode.mkdir()
+
+	def execute(self):
+		"""
+		Restore the data from previous builds and call :py:meth:`waflib.Build.BuildContext.execute_build`. Overrides from :py:func:`waflib.Context.Context.execute`
+		"""
+		self.restore()
+		if not self.all_envs:
+			self.load_envs()
+
+		self.execute_build()
+
+	def execute_build(self):
+		"""
+		Execute the build by:
+
+		* reading the scripts (see :py:meth:`waflib.Context.Context.recurse`)
+		* calling :py:meth:`waflib.Build.BuildContext.pre_build` to call user build functions
+		* calling :py:meth:`waflib.Build.BuildContext.compile` to process the tasks
+		* calling :py:meth:`waflib.Build.BuildContext.post_build` to call user build functions
+		"""
+
+		Logs.info("Waf: Entering directory `%s'" % self.variant_dir)
+		self.recurse([self.run_dir])
+		self.pre_build()
+
+		# display the time elapsed in the progress bar
+		self.timer = Utils.Timer()
+
+		if self.progress_bar:
+			sys.stderr.write(Logs.colors.cursor_off)
+		try:
+			self.compile()
+		finally:
+			if self.progress_bar == 1:
+				c = len(self.returned_tasks) or 1
+				self.to_log(self.progress_line(c, c, Logs.colors.BLUE, Logs.colors.NORMAL))
+				print('')
+				sys.stdout.flush()
+				sys.stderr.write(Logs.colors.cursor_on)
+			Logs.info("Waf: Leaving directory `%s'" % self.variant_dir)
+		self.post_build()
+
+	def restore(self):
+		"""
+		Load the data from a previous run, sets the attributes listed in :py:const:`waflib.Build.SAVED_ATTRS`
+		"""
+		try:
+			env = ConfigSet.ConfigSet(os.path.join(self.cache_dir, 'build.config.py'))
+		except (IOError, OSError):
+			pass
+		else:
+			if env['version'] < Context.HEXVERSION:
+				raise Errors.WafError('Version mismatch! reconfigure the project')
+			for t in env['tools']:
+				self.setup(**t)
+
+		f = None
+		try:
+			try:
+				f = open(os.path.join(self.variant_dir, Context.DBFILE), 'rb')
+			except (IOError, EOFError):
+				# handle missing file/empty file
+				Logs.debug('build: could not load the build cache (missing)')
+			else:
+				try:
+					waflib.Node.pickle_lock.acquire()
+					waflib.Node.Nod3 = self.node_class
+					try:
+						data = cPickle.load(f)
+					except Exception as e:
+						Logs.debug('build: could not load the build cache %r' % e)
+					else:
+						for x in SAVED_ATTRS:
+							setattr(self, x, data[x])
+				finally:
+					waflib.Node.pickle_lock.release()
+		finally:
+			if f:
+				f.close()
+
+		self.init_dirs()
+
+	def store(self):
+		"""
+		Store the data for next runs, sets the attributes listed in :py:const:`waflib.Build.SAVED_ATTRS`. Uses a temporary
+		file to avoid problems on ctrl+c.
+		"""
+
+		data = {}
+		for x in SAVED_ATTRS:
+			data[x] = getattr(self, x)
+		db = os.path.join(self.variant_dir, Context.DBFILE)
+
+		try:
+			waflib.Node.pickle_lock.acquire()
+			waflib.Node.Nod3 = self.node_class
+
+			f = None
+			try:
+				f = open(db + '.tmp', 'wb')
+				cPickle.dump(data, f)
+			finally:
+				if f:
+					f.close()
+		finally:
+			waflib.Node.pickle_lock.release()
+
+		try:
+			st = os.stat(db)
+			os.unlink(db)
+			if sys.platform != 'win32': # win32 has no chown but we're paranoid
+				os.chown(db + '.tmp', st.st_uid, st.st_gid)
+		except (AttributeError, OSError):
+			pass
+
+		# do not use shutil.move (copy is not thread-safe)
+		os.rename(db + '.tmp', db)
+
+	def compile(self):
+		"""
+		Run the build by creating an instance of :py:class:`waflib.Runner.Parallel`
+		The cache file is not written if the build is up to date (no task executed).
+		"""
+		Logs.debug('build: compile()')
+
+		# use another object to perform the producer-consumer logic (reduce the complexity)
+		self.producer = Runner.Parallel(self, self.jobs)
+		self.producer.biter = self.get_build_iterator()
+		self.returned_tasks = [] # not part of the API yet
+		try:
+			self.producer.start()
+		except KeyboardInterrupt:
+			self.store()
+			raise
+		else:
+			if self.producer.dirty:
+				self.store()
+
+		if self.producer.error:
+			raise Errors.BuildError(self.producer.error)
+
+	def setup(self, tool, tooldir=None, funs=None):
+		"""
+		Import waf tools, used to import those accessed during the configuration::
+
+			def configure(conf):
+				conf.load('glib2')
+
+			def build(bld):
+				pass # glib2 is imported implicitly
+
+		:param tool: tool list
+		:type tool: list
+		:param tooldir: optional tool directory (sys.path)
+		:type tooldir: list of string
+		:param funs: unused variable
+		"""
+		if isinstance(tool, list):
+			for i in tool: self.setup(i, tooldir)
+			return
+
+		module = Context.load_tool(tool, tooldir)
+		if hasattr(module, "setup"): module.setup(self)
+
+	def get_env(self):
+		"""Getter for the env property"""
+		try:
+			return self.all_envs[self.variant]
+		except KeyError:
+			return self.all_envs['']
+	def set_env(self, val):
+		"""Setter for the env property"""
+		self.all_envs[self.variant] = val
+
+	env = property(get_env, set_env)
+
+	def add_manual_dependency(self, path, value):
+		"""
+		Adds a dependency from a node object to a value::
+
+			def build(bld):
+				bld.add_manual_dependency(
+					bld.path.find_resource('wscript'),
+					bld.root.find_resource('/etc/fstab'))
+
+		:param path: file path
+		:type path: string or :py:class:`waflib.Node.Node`
+		:param value: value to depend on
+		:type value: :py:class:`waflib.Node.Node`, string, or function returning a string
+		"""
+		if isinstance(path, waflib.Node.Node):
+			node = path
+		elif os.path.isabs(path):
+			node = self.root.find_resource(path)
+		else:
+			node = self.path.find_resource(path)
+		self.deps_man[id(node)].append(value)
+
+	def launch_node(self):
+		"""Returns the launch directory as a :py:class:`waflib.Node.Node` object"""
+		try:
+			# private cache
+			return self.p_ln
+		except AttributeError:
+			self.p_ln = self.root.find_dir(self.launch_dir)
+			return self.p_ln
+
+	def hash_env_vars(self, env, vars_lst):
+		"""
+		Hash configuration set variables::
+
+			def build(bld):
+				bld.hash_env_vars(bld.env, ['CXX', 'CC'])
+
+		:param env: Configuration Set
+		:type env: :py:class:`waflib.ConfigSet.ConfigSet`
+		:param vars_lst: list of variables
+		:type vars_list: list of string
+		"""
+
+		if not env.table:
+			env = env.parent
+			if not env:
+				return Utils.SIG_NIL
+
+		idx = str(id(env)) + str(vars_lst)
+		try:
+			cache = self.cache_env
+		except AttributeError:
+			cache = self.cache_env = {}
+		else:
+			try:
+				return self.cache_env[idx]
+			except KeyError:
+				pass
+
+		lst = [env[a] for a in vars_lst]
+		ret = Utils.h_list(lst)
+		Logs.debug('envhash: %r %r', ret, lst)
+
+		cache[idx] = ret
+
+		return ret
+
+	def get_tgen_by_name(self, name):
+		"""
+		Retrieves a task generator from its name or its target name
+		the name must be unique::
+
+			def build(bld):
+				tg = bld(name='foo')
+				tg == bld.get_tgen_by_name('foo')
+		"""
+		cache = self.task_gen_cache_names
+		if not cache:
+			# create the index lazily
+			for g in self.groups:
+				for tg in g:
+					try:
+						cache[tg.name] = tg
+					except AttributeError:
+						# raised if not a task generator, which should be uncommon
+						pass
+		try:
+			return cache[name]
+		except KeyError:
+			raise Errors.WafError('Could not find a task generator for the name %r' % name)
+
+	def progress_line(self, state, total, col1, col2):
+		"""
+		Compute the progress bar used by ``waf -p``
+		"""
+		n = len(str(total))
+
+		Utils.rot_idx += 1
+		ind = Utils.rot_chr[Utils.rot_idx % 4]
+
+		pc = (100.*state)/total
+		eta = str(self.timer)
+		fs = "[%%%dd/%%%dd][%%s%%2d%%%%%%s][%s][" % (n, n, ind)
+		left = fs % (state, total, col1, pc, col2)
+		right = '][%s%s%s]' % (col1, eta, col2)
+
+		cols = Logs.get_term_cols() - len(left) - len(right) + 2*len(col1) + 2*len(col2)
+		if cols < 7: cols = 7
+
+		ratio = ((cols*state)//total) - 1
+
+		bar = ('='*ratio+'>').ljust(cols)
+		msg = Utils.indicator % (left, bar, right)
+
+		return msg
+
+	def declare_chain(self, *k, **kw):
+		"""
+		Wrapper for :py:func:`waflib.TaskGen.declare_chain` provided for convenience
+		"""
+		return TaskGen.declare_chain(*k, **kw)
+
+	def pre_build(self):
+		"""Execute user-defined methods before the build starts, see :py:meth:`waflib.Build.BuildContext.add_pre_fun`"""
+		for m in getattr(self, 'pre_funs', []):
+			m(self)
+
+	def post_build(self):
+		"""Executes the user-defined methods after the build is successful, see :py:meth:`waflib.Build.BuildContext.add_post_fun`"""
+		for m in getattr(self, 'post_funs', []):
+			m(self)
+
+	def add_pre_fun(self, meth):
+		"""
+		Bind a method to execute after the scripts are read and before the build starts::
+
+			def mycallback(bld):
+				print("Hello, world!")
+
+			def build(bld):
+				bld.add_pre_fun(mycallback)
+		"""
+		try:
+			self.pre_funs.append(meth)
+		except AttributeError:
+			self.pre_funs = [meth]
+
+	def add_post_fun(self, meth):
+		"""
+		Bind a method to execute immediately after the build is successful::
+
+			def call_ldconfig(bld):
+				bld.exec_command('/sbin/ldconfig')
+
+			def build(bld):
+				if bld.cmd == 'install':
+					bld.add_pre_fun(call_ldconfig)
+		"""
+		try:
+			self.post_funs.append(meth)
+		except AttributeError:
+			self.post_funs = [meth]
+
+	def get_group(self, x):
+		"""
+		Get the group x, or return the current group if x is None
+
+		:param x: name or number or None
+		:type x: string, int or None
+		"""
+		if not self.groups:
+			self.add_group()
+		if x is None:
+			return self.groups[self.current_group]
+		if x in self.group_names:
+			return self.group_names[x]
+		return self.groups[x]
+
+	def add_to_group(self, tgen, group=None):
+		"""add a task or a task generator for the build"""
+		# paranoid
+		assert(isinstance(tgen, TaskGen.task_gen) or isinstance(tgen, Task.TaskBase))
+		tgen.bld = self
+		self.get_group(group).append(tgen)
+
+	def get_group_name(self, g):
+		"""name for the group g (utility)"""
+		if not isinstance(g, list):
+			g = self.groups[g]
+		for x in self.group_names:
+			if id(self.group_names[x]) == id(g):
+				return x
+		return ''
+
+	def get_group_idx(self, tg):
+		"""
+		Index of the group containing the task generator given as argument::
+
+			def build(bld):
+				tg = bld(name='nada')
+				0 == bld.get_group_idx(tg)
+
+		:param tg: Task generator object
+		:type tg: :py:class:`waflib.TaskGen.task_gen`
+		"""
+		se = id(tg)
+		for i in range(len(self.groups)):
+			for t in self.groups[i]:
+				if id(t) == se:
+					return i
+		return None
+
+	def add_group(self, name=None, move=True):
+		"""
+		Add a new group of tasks/task generators. By default the new group becomes the default group for new task generators.
+
+		:param name: name for this group
+		:type name: string
+		:param move: set the group created as default group (True by default)
+		:type move: bool
+		"""
+		#if self.groups and not self.groups[0].tasks:
+		#	error('add_group: an empty group is already present')
+		if name and name in self.group_names:
+			Logs.error('add_group: name %s already present' % name)
+		g = []
+		self.group_names[name] = g
+		self.groups.append(g)
+		if move:
+			self.current_group = len(self.groups) - 1
+
+	def set_group(self, idx):
+		"""
+		Set the current group to be idx: now new task generators will be added to this group by default::
+
+			def build(bld):
+				bld(rule='touch ${TGT}', target='foo.txt')
+				bld.add_group() # now the current group is 1
+				bld(rule='touch ${TGT}', target='bar.txt')
+				bld.set_group(0) # now the current group is 0
+				bld(rule='touch ${TGT}', target='truc.txt') # build truc.txt before bar.txt
+
+		:param idx: group name or group index
+		:type idx: string or int
+		"""
+		if isinstance(idx, str):
+			g = self.group_names[idx]
+			for i in range(len(self.groups)):
+				if id(g) == id(self.groups[i]):
+					self.current_group = i
+		else:
+			self.current_group = idx
+
+	def total(self):
+		"""
+		Approximate task count: this value may be inaccurate if task generators are posted lazily (see :py:attr:`waflib.Build.BuildContext.post_mode`).
+		The value :py:attr:`waflib.Runner.Parallel.total` is updated during the task execution.
+		"""
+		total = 0
+		for group in self.groups:
+			for tg in group:
+				try:
+					total += len(tg.tasks)
+				except AttributeError:
+					total += 1
+		return total
+
+	def get_targets(self):
+		"""
+		Return the task generator corresponding to the 'targets' list, used by :py:meth:`waflib.Build.BuildContext.get_build_iterator`::
+
+			$ waf --targets=myprogram,myshlib
+		"""
+		to_post = []
+		min_grp = 0
+		for name in self.targets.split(','):
+			tg = self.get_tgen_by_name(name)
+			if not tg:
+				raise Errors.WafError('target %r does not exist' % name)
+
+			m = self.get_group_idx(tg)
+			if m > min_grp:
+				min_grp = m
+				to_post = [tg]
+			elif m == min_grp:
+				to_post.append(tg)
+		return (min_grp, to_post)
+
+	def post_group(self):
+		"""
+		Post the task generators from the group indexed by self.cur, used by :py:meth:`waflib.Build.BuildContext.get_build_iterator`
+		"""
+		if self.targets == '*':
+			for tg in self.groups[self.cur]:
+				try:
+					f = tg.post
+				except AttributeError:
+					pass
+				else:
+					f()
+		elif self.targets:
+			if self.cur < self._min_grp:
+				for tg in self.groups[self.cur]:
+					try:
+						f = tg.post
+					except AttributeError:
+						pass
+					else:
+						f()
+			else:
+				for tg in self._exact_tg:
+					tg.post()
+		else:
+			ln = self.launch_node()
+			for tg in self.groups[self.cur]:
+				try:
+					f = tg.post
+				except AttributeError:
+					pass
+				else:
+					if tg.path.is_child_of(ln):
+						f()
+
+	def get_tasks_group(self, idx):
+		"""
+		Return all the tasks for the group of num idx, used by :py:meth:`waflib.Build.BuildContext.get_build_iterator`
+		"""
+		tasks = []
+		for tg in self.groups[idx]:
+			# TODO a try-except might be more efficient
+			if isinstance(tg, Task.TaskBase):
+				tasks.append(tg)
+			else:
+				tasks.extend(tg.tasks)
+		return tasks
+
+	def get_build_iterator(self):
+		"""
+		Creates a generator object that returns lists of tasks executable in parallel (yield)
+
+		:return: tasks which can be executed immediatly
+		:rtype: list of :py:class:`waflib.Task.TaskBase`
+		"""
+		self.cur = 0
+
+		if self.targets and self.targets != '*':
+			(self._min_grp, self._exact_tg) = self.get_targets()
+
+		global lazy_post
+		if self.post_mode != POST_LAZY:
+			while self.cur < len(self.groups):
+				self.post_group()
+				self.cur += 1
+			self.cur = 0
+
+		while self.cur < len(self.groups):
+			# first post the task generators for the group
+			if self.post_mode != POST_AT_ONCE:
+				self.post_group()
+
+			# then extract the tasks
+			tasks = self.get_tasks_group(self.cur)
+			# if the constraints are set properly (ext_in/ext_out, before/after)
+			# the call to set_file_constraints may be removed (can be a 15% penalty on no-op rebuilds)
+			# (but leave set_file_constraints for the installation step)
+			#
+			# if the tasks have only files, set_file_constraints is required but set_precedence_constraints is not necessary
+			#
+			Task.set_file_constraints(tasks)
+			Task.set_precedence_constraints(tasks)
+
+			self.cur_tasks = tasks
+			self.cur += 1
+			if not tasks: # return something else the build will stop
+				continue
+			yield tasks
+		while 1:
+			yield []
+
+
+	#def install_dir(self, path, env=None):
+	#	"""
+	#	Create empty folders for the installation (very rarely used) TODO
+	#	"""
+	#	return
+
+class inst(Task.Task):
+	"""
+    Special task used for installing files and symlinks, it behaves both like a task
+	and like a task generator
+	"""
+	color = 'CYAN'
+
+	def post(self):
+		"""
+		Same interface as in :py:meth:`waflib.TaskGen.task_gen.post`
+		"""
+		buf = []
+		for x in self.source:
+			if isinstance(x, waflib.Node.Node):
+				y = x
+			else:
+				y = self.path.find_resource(x)
+				if not y:
+					idx = self.generator.bld.get_group_idx(self)
+					for tg in self.generator.bld.groups[idx]:
+						if not isinstance(tg, inst) and id(tg) != id(self):
+							tg.post()
+						y = self.path.find_resource(x)
+						if y:
+							break
+					else:
+						raise Errors.WafError('could not find %r in %r' % (x, self.path))
+			buf.append(y)
+		self.inputs = buf
+
+	def runnable_status(self):
+		"""
+		Installation tasks are always executed, so this method returns either :py:const:`waflib.Task.ASK_LATER` or :py:const:`waflib.Task.RUN_ME`.
+		"""
+		ret = super(inst, self).runnable_status()
+		if ret == Task.SKIP_ME:
+			return Task.RUN_ME
+		return ret
+
+	def __str__(self):
+		"""Return an empty string to disable the display"""
+		return ''
+
+	def run(self):
+		"""The attribute 'exec_task' holds the method to execute"""
+		return self.generator.exec_task()
+
+	def get_install_path(self):
+		"""
+		Installation path obtained from ``self.dest`` and prefixed by the destdir.
+		The variables such as '${PREFIX}/bin' are substituted.
+		"""
+		dest = self.dest.replace('/', os.sep)
+		dest = Utils.subst_vars(self.dest, self.env)
+		if Options.options.destdir:
+			dest = os.path.join(Options.options.destdir, os.path.splitdrive(dest)[1].lstrip(os.sep))
+		return dest
+
+	def exec_install_files(self):
+		"""
+		Predefined method for installing files
+		"""
+		destpath = self.get_install_path()
+		if not destpath:
+			raise Errors.WafError('unknown installation path %r' % self.generator)
+		for x, y in zip(self.source, self.inputs):
+			if self.relative_trick:
+				destfile = os.path.join(destpath, y.path_from(self.path))
+				Utils.check_dir(os.path.dirname(destfile))
+			else:
+				destfile = os.path.join(destpath, y.name)
+			self.generator.bld.do_install(y.abspath(), destfile, self.chmod)
+
+	def exec_install_as(self):
+		"""
+		Predefined method for installing one file with a given name
+		"""
+		destfile = self.get_install_path()
+		self.generator.bld.do_install(self.inputs[0].abspath(), destfile, self.chmod)
+
+	def exec_symlink_as(self):
+		"""
+		Predefined method for installing a symlink
+		"""
+		destfile = self.get_install_path()
+		self.generator.bld.do_link(self.link, destfile)
+
+class InstallContext(BuildContext):
+	'''installs the targets on the system'''
+	cmd = 'install'
+
+	def __init__(self, **kw):
+		super(InstallContext, self).__init__(**kw)
+
+		# list of targets to uninstall for removing the empty folders after uninstalling
+		self.uninstall = []
+		self.is_install = INSTALL
+
+	def do_install(self, src, tgt, chmod=Utils.O644):
+		"""
+		Copy a file from src to tgt with given file permissions. The actual copy is not performed
+		if the source and target file have the same size and the same timestamps. When the copy occurs,
+		the file is first removed and then copied (prevent stale inodes).
+
+		This method is overridden in :py:meth:`waflib.Build.UninstallContext.do_install` to remove the file.
+
+		:param src: file name as absolute path
+		:type src: string
+		:param tgt: file destination, as absolute path
+		:type tgt: string
+		:param chmod: installation mode
+		:type chmod: int
+		"""
+		d, _ = os.path.split(tgt)
+		if not d:
+			raise Errors.WafError('Invalid installation given %r->%r' % (src, tgt))
+		Utils.check_dir(d)
+
+		srclbl = src.replace(self.srcnode.abspath() + os.sep, '')
+		if not Options.options.force:
+			# check if the file is already there to avoid a copy
+			try:
+				st1 = os.stat(tgt)
+				st2 = os.stat(src)
+			except OSError:
+				pass
+			else:
+				# same size and identical timestamps -> make no copy
+				if st1.st_mtime >= st2.st_mtime and st1.st_size == st2.st_size:
+					if not self.progress_bar:
+						Logs.info('- install %s (from %s)' % (tgt, srclbl))
+					return False
+
+		if not self.progress_bar:
+			Logs.info('+ install %s (from %s)' % (tgt, srclbl))
+
+		# following is for shared libs and stale inodes (-_-)
+		try:
+			os.remove(tgt)
+		except OSError:
+			pass
+
+		try:
+			shutil.copy2(src, tgt)
+			os.chmod(tgt, chmod)
+		except IOError:
+			try:
+				os.stat(src)
+			except (OSError, IOError):
+				Logs.error('File %r does not exist' % src)
+			raise Errors.WafError('Could not install the file %r' % tgt)
+
+	def do_link(self, src, tgt):
+		"""
+		Create a symlink from tgt to src.
+
+		This method is overridden in :py:meth:`waflib.Build.UninstallContext.do_link` to remove the symlink.
+
+		:param src: file name as absolute path
+		:type src: string
+		:param tgt: file destination, as absolute path
+		:type tgt: string
+		"""
+		d, _ = os.path.split(tgt)
+		Utils.check_dir(d)
+
+		link = False
+		if not os.path.islink(tgt):
+			link = True
+		elif os.readlink(tgt) != src:
+			link = True
+
+		if link:
+			try: os.remove(tgt)
+			except OSError: pass
+			if not self.progress_bar:
+				Logs.info('+ symlink %s (to %s)' % (tgt, src))
+			os.symlink(src, tgt)
+		else:
+			if not self.progress_bar:
+				Logs.info('- symlink %s (to %s)' % (tgt, src))
+
+	def run_task_now(self, tsk, postpone):
+		"""
+		This method is called by :py:meth:`waflib.Build.InstallContext.install_files`,
+		:py:meth:`waflib.Build.InstallContext.install_as` and :py:meth:`waflib.Build.InstallContext.symlink_as` immediately
+		after the installation task is created. Its role is to force the immediate execution if necessary, that is when
+		``postpone=False`` was given.
+		"""
+		tsk.post()
+		if not postpone:
+			if tsk.runnable_status() == Task.ASK_LATER:
+				raise self.WafError('cannot post the task %r' % tsk)
+			tsk.run()
+
+	def install_files(self, dest, files, env=None, chmod=Utils.O644, relative_trick=False, cwd=None, add=True, postpone=True):
+		"""
+		Create a task to install files on the system::
+
+			def build(bld):
+				bld.install_files('${DATADIR}', self.path.find_resource('wscript'))
+
+		:param dest: absolute path of the destination directory
+		:type dest: string
+		:param files: input files
+		:type files: list of strings or list of nodes
+		:param env: configuration set for performing substitutions in dest
+		:type env: Configuration set
+		:param relative_trick: preserve the folder hierarchy when installing whole folders
+		:type relative_trick: bool
+		:param cwd: parent node for searching srcfile, when srcfile is not a :py:class:`waflib.Node.Node`
+		:type cwd: :py:class:`waflib.Node.Node`
+		:param add: add the task created to a build group - set ``False`` only if the installation task is created after the build has started
+		:type add: bool
+		:param postpone: execute the task immediately to perform the installation
+		:type postpone: bool
+		"""
+		tsk = inst(env=env or self.env)
+		tsk.bld = self
+		tsk.path = cwd or self.path
+		tsk.chmod = chmod
+		if isinstance(files, waflib.Node.Node):
+			tsk.source =  [files]
+		else:
+			tsk.source = Utils.to_list(files)
+		tsk.dest = dest
+		tsk.exec_task = tsk.exec_install_files
+		tsk.relative_trick = relative_trick
+		if add: self.add_to_group(tsk)
+		self.run_task_now(tsk, postpone)
+		return tsk
+
+	def install_as(self, dest, srcfile, env=None, chmod=Utils.O644, cwd=None, add=True, postpone=True):
+		"""
+		Create a task to install a file on the system with a different name::
+
+			def build(bld):
+				bld.install_as('${PREFIX}/bin', 'myapp', chmod=Utils.O755)
+
+		:param dest: absolute path of the destination file
+		:type dest: string
+		:param srcfile: input file
+		:type srcfile: string or node
+		:param cwd: parent node for searching srcfile, when srcfile is not a :py:class:`waflib.Node.Node`
+		:type cwd: :py:class:`waflib.Node.Node`
+		:param env: configuration set for performing substitutions in dest
+		:type env: Configuration set
+		:param add: add the task created to a build group - set ``False`` only if the installation task is created after the build has started
+		:type add: bool
+		:param postpone: execute the task immediately to perform the installation
+		:type postpone: bool
+		"""
+		tsk = inst(env=env or self.env)
+		tsk.bld = self
+		tsk.path = cwd or self.path
+		tsk.chmod = chmod
+		tsk.source = [srcfile]
+		tsk.dest = dest
+		tsk.exec_task = tsk.exec_install_as
+		if add: self.add_to_group(tsk)
+		self.run_task_now(tsk, postpone)
+		return tsk
+
+	def symlink_as(self, dest, src, env=None, cwd=None, add=True, postpone=True):
+		"""
+		Create a task to install a symlink::
+
+			def build(bld):
+				bld.symlink_as('${PREFIX}/lib/libfoo.so', 'libfoo.so.1.2.3')
+
+		:param dest: absolute path of the symlink
+		:type dest: string
+		:param src: absolute or relative path of the link
+		:type src: string
+		:param env: configuration set for performing substitutions in dest
+		:type env: Configuration set
+		:param add: add the task created to a build group - set ``False`` only if the installation task is created after the build has started
+		:type add: bool
+		:param postpone: execute the task immediately to perform the installation
+		:type postpone: bool
+		"""
+
+		if sys.platform == 'win32':
+			# symlinks *cannot* work on that platform
+			return
+
+		tsk = inst(env=env or self.env)
+		tsk.bld = self
+		tsk.dest = dest
+		tsk.path = cwd or self.path
+		tsk.source = []
+		tsk.link = src
+		tsk.exec_task = tsk.exec_symlink_as
+		if add: self.add_to_group(tsk)
+		self.run_task_now(tsk, postpone)
+		return tsk
+
+class UninstallContext(InstallContext):
+	'''removes the targets installed'''
+	cmd = 'uninstall'
+
+	def __init__(self, **kw):
+		super(UninstallContext, self).__init__(**kw)
+		self.is_install = UNINSTALL
+
+	def do_install(self, src, tgt, chmod=Utils.O644):
+		"""See :py:meth:`waflib.Build.InstallContext.do_install`"""
+		if not self.progress_bar:
+			Logs.info('- remove %s' % tgt)
+
+		self.uninstall.append(tgt)
+		try:
+			os.remove(tgt)
+		except OSError as e:
+			if e.errno != errno.ENOENT:
+				if not getattr(self, 'uninstall_error', None):
+					self.uninstall_error = True
+					Logs.warn('build: some files could not be uninstalled (retry with -vv to list them)')
+				if Logs.verbose > 1:
+					Logs.warn('could not remove %s (error code %r)' % (e.filename, e.errno))
+
+		# TODO ita refactor this into a post build action to uninstall the folders (optimization)
+		while tgt:
+			tgt = os.path.dirname(tgt)
+			try:
+				os.rmdir(tgt)
+			except OSError:
+				break
+
+	def do_link(self, src, tgt):
+		"""See :py:meth:`waflib.Build.InstallContext.do_link`"""
+		try:
+			if not self.progress_bar:
+				Logs.info('- unlink %s' % tgt)
+			os.remove(tgt)
+		except OSError:
+			pass
+
+		# TODO ita refactor this into a post build action to uninstall the folders (optimization)?
+		while tgt:
+			tgt = os.path.dirname(tgt)
+			try:
+				os.rmdir(tgt)
+			except OSError:
+				break
+
+	def execute(self):
+		"""
+		See :py:func:`waflib.Context.Context.execute`
+		"""
+		try:
+			# do not execute any tasks
+			def runnable_status(self):
+				return Task.SKIP_ME
+			setattr(Task.Task, 'runnable_status_back', Task.Task.runnable_status)
+			setattr(Task.Task, 'runnable_status', runnable_status)
+
+			super(UninstallContext, self).execute()
+		finally:
+			setattr(Task.Task, 'runnable_status', Task.Task.runnable_status_back)
+
+class CleanContext(BuildContext):
+	'''cleans the project'''
+	cmd = 'clean'
+	def execute(self):
+		"""
+		See :py:func:`waflib.Context.Context.execute`
+		"""
+		self.restore()
+		if not self.all_envs:
+			self.load_envs()
+
+		self.recurse([self.run_dir])
+		try:
+			self.clean()
+		finally:
+			self.store()
+
+	def clean(self):
+		"""clean the data and some files in the build dir .. well, TODO"""
+		Logs.debug('build: clean called')
+
+		if self.bldnode != self.srcnode:
+			# would lead to a disaster if top == out
+			lst = [self.root.find_or_declare(f) for f in self.env[CFG_FILES]]
+			for n in self.bldnode.ant_glob('**/*', excl='lock* *conf_check_*/** config.log c4che/*'):
+				if n in lst:
+					continue
+				n.delete()
+		self.root.children = {}
+
+		for v in 'node_deps task_sigs raw_deps'.split():
+			setattr(self, v, {})
+
+class ListContext(BuildContext):
+	'''lists the targets to execute'''
+
+	cmd = 'list'
+	def execute(self):
+		"""
+		See :py:func:`waflib.Context.Context.execute`.
+		"""
+		self.restore()
+		if not self.all_envs:
+			self.load_envs()
+
+		self.recurse([self.run_dir])
+		self.pre_build()
+
+		# display the time elapsed in the progress bar
+		self.timer = Utils.Timer()
+
+		for g in self.groups:
+			for tg in g:
+				try:
+					f = tg.post
+				except AttributeError:
+					pass
+				else:
+					f()
+
+		try:
+			# force the cache initialization
+			self.get_tgen_by_name('')
+		except:
+			pass
+		lst = list(self.task_gen_cache_names.keys())
+		lst.sort()
+		for k in lst:
+			Logs.pprint('GREEN', k)
+
+class StepContext(BuildContext):
+	'''executes tasks in a step-by-step fashion, for debugging'''
+	cmd = 'step'
+
+	def __init__(self, **kw):
+		super(StepContext, self).__init__(**kw)
+		self.files = Options.options.files
+
+	def compile(self):
+		"""
+		Compile the tasks matching the input/output files given (regular expression matching). Derived from :py:meth:`waflib.Build.BuildContext.compile`::
+
+			$ waf step --files=foo.c,bar.c,in:truc.c,out:bar.o
+			$ waf step --files=in:foo.cpp.1.o # link task only
+
+		"""
+		if not self.files:
+			Logs.warn('Add a pattern for the debug build, for example "waf step --files=main.c,app"')
+			BuildContext.compile(self)
+			return
+
+		for g in self.groups:
+			for tg in g:
+				try:
+					f = tg.post
+				except AttributeError:
+					pass
+				else:
+					f()
+
+		for pat in self.files.split(','):
+			inn = True
+			out = True
+			if pat.startswith('in:'):
+				out = False
+				pat = pat.replace('in:', '')
+			elif pat.startswith('out:'):
+				inn = False
+				pat = pat.replace('out:', '')
+
+			if not pat.startswith('^'):
+				pat = '^.+?%s' % pat
+			if not pat.endswith('$'):
+				pat = '%s$' % pat
+			pat = re.compile(pat)
+
+			for g in self.groups:
+				for tg in g:
+					if isinstance(tg, Task.TaskBase):
+						lst = [tg]
+					else:
+						lst = tg.tasks
+					for tsk in lst:
+						do_exec = False
+						if inn:
+							for node in getattr(tsk, 'inputs', []):
+								if pat.match(node.abspath()):
+									do_exec = True
+									break
+						if out and not do_exec:
+							for node in getattr(tsk, 'outputs', []):
+								if pat.match(node.abspath()):
+									do_exec = True
+									break
+						if do_exec:
+							ret = tsk.run()
+							Logs.info('%s -> %r' % (str(tsk), ret))
+
+BuildContext.store = Utils.nogc(BuildContext.store)
+BuildContext.restore = Utils.nogc(BuildContext.restore)
+

File waflib/ConfigSet.py

+#!/usr/bin/env python
+# encoding: utf-8
+# Thomas Nagy, 2005-2010 (ita)
+
+"""
+
+ConfigSet: a special dict
+
+The values put in :py:class:`ConfigSet` must be lists
+"""
+
+import os, copy, re
+from waflib import Logs, Utils
+re_imp = re.compile('^(#)*?([^#=]*?)\ =\ (.*?)$', re.M)
+
+class ConfigSet(object):
+	"""
+	A dict that honor serialization and parent relationships. The serialization format
+	is human-readable (python-like) and performed by using eval() and repr().
+	For high performance prefer pickle. Do not store functions as they are not serializable.
+
+	The values can be accessed by attributes or by keys::
+
+		from waflib.ConfigSet import ConfigSet
+		env = ConfigSet()
+		env.FOO = 'test'
+		env['FOO'] = 'test'
+	"""
+	__slots__ = ('table', 'parent')
+	def __init__(self, filename=None):
+		self.table = {}
+		"""
+		Internal dict holding the object values
+		"""
+		#self.parent = None
+
+		if filename:
+			self.load(filename)
+
+	def __contains__(self, key):
+		"""
+		Enable the *in* syntax::
+
+			if 'foo' in env:
+				print env['foo']
+		"""
+		if key in self.table: return True
+		try: return self.parent.__contains__(key)
+		except AttributeError: return False # parent may not exist
+
+	def __str__(self):
+		"""Text representation of the ConfigSet (for debugging purposes)"""
+		keys = set()
+		cur = self
+		while cur:
+			keys.update(cur.table.keys())
+			cur = getattr(cur, 'parent', None)
+		keys = list(keys)
+		keys.sort()
+		return "\n".join(["%r %r" % (x, self.__getitem__(x)) for x in keys])
+
+	def __getitem__(self, key):
+		"""
+		Dictionary interface: get value from key::
+
+			def configure(conf):
+				conf.env['foo'] = {}
+				print(env['foo'])
+		"""
+		try:
+			while 1:
+				x = self.table.get(key, None)
+				if not x is None:
+					return x
+				self = self.parent
+		except AttributeError:
+			return []
+
+	def __setitem__(self, key, value):
+		"""
+		Dictionary interface: get value from key
+		"""
+		self.table[key] = value
+
+	def __delitem__(self, key, value):
+		"""
+		Dictionary interface: get value from key
+		"""
+		del self.table[key]
+
+	def __getattr__(self, name):
+		"""
+		Attribute access provided for convenience. The following forms are equivalent::
+
+			def configure(conf):
+				conf.env.value
+				conf.env['value']
+		"""
+		if name in self.__slots__:
+			return object.__getattr__(self, name)
+		else:
+			return self[name]
+
+	def __setattr__(self, name, value):
+		"""
+		Attribute access provided for convenience. The following forms are equivalent::
+
+			def configure(conf):
+				conf.env.value = x
+				env['value'] = x
+		"""
+		if name in self.__slots__:
+			object.__setattr__(self, name, value)
+		else:
+			self[name] = value
+
+	def __delattr__(self, name):
+		"""
+		Attribute access provided for convenience. The following forms are equivalent::
+
+			def configure(conf):
+				del env.value
+				del env['value']
+		"""
+		if name in self.__slots__:
+			object.__delattr__(self, name)
+		else:
+			del self[name]
+
+	def derive(self):
+		"""
+		Returns a new ConfigSet deriving from self. The copy returned
+		will be a shallow copy::
+
+			from waflib.ConfigSet import ConfigSet
+			env = ConfigSet()
+			env.append_value('CFLAGS', ['-O2'])
+			child = env.derive()
+			child.CFLAGS.append('test') # warning! this will modify 'env'
+			child.CFLAGS = ['-O3'] # new list, ok
+			child.append_value('CFLAGS', ['-O3']) # ok
+
+		Use :py:func:`ConfigSet.detach` to detach the child from the parent.
+		"""
+		newenv = ConfigSet()
+		newenv.parent = self
+		return newenv
+
+	def detach(self):
+		"""
+		Detach self from its parent (if existing)
+
+		Modifying the parent :py:class:`ConfigSet` will not change the current object
+		Modifying this :py:class:`ConfigSet` will not modify the parent one.
+		"""
+		tbl = self.get_merged_dict()
+		try:
+			delattr(self, 'parent')
+		except AttributeError:
+			pass
+		else:
+			keys = tbl.keys()
+			for x in keys:
+				tbl[x] = copy.deepcopy(tbl[x])
+			self.table = tbl
+
+	def get_flat(self, key):
+		"""
+		Return a value as a string. If the input is a list, the value returned is space-separated.
+
+		:param key: key to use
+		:type key: string
+		"""
+		s = self[key]
+		if isinstance(s, str): return s
+		return ' '.join(s)
+
+	def _get_list_value_for_modification(self, key):
+		"""
+		Return a list value for further modification.
+
+		The list may be modified inplace and there is no need to do this afterwards::
+
+			self.table[var] = value
+		"""
+		try:
+			value = self.table[key]
+		except KeyError:
+			try: value = self.parent[key]
+			except AttributeError: value = []
+			if isinstance(value, list):
+				value = value[:]
+			else:
+				value = [value]
+		else:
+			if not isinstance(value, list):
+				value = [value]
+		self.table[key] = value
+		return value
+
+	def append_value(self, var, val):
+		"""
+		Appends a value to the specified config key::
+
+			def build(bld):
+				bld.env.append_value('CFLAGS', ['-O2'])
+
+		The value must be a list or a tuple
+		"""
+		current_value = self._get_list_value_for_modification(var)
+		if isinstance(val, str): # if there were string everywhere we could optimize this
+			val = [val]
+		current_value.extend(val)
+
+	def prepend_value(self, var, val):
+		"""
+		Prepends a value to the specified item::
+
+			def configure(conf):
+				conf.env.prepend_value('CFLAGS', ['-O2'])
+
+		The value must be a list or a tuple
+		"""
+		if isinstance(val, str):
+			val = [val]
+		self.table[var] =  val + self._get_list_value_for_modification(var)
+
+	def append_unique(self, var, val):
+		"""
+		Append a value to the specified item only if it's not already present::
+
+			def build(bld):
+				bld.env.append_unique('CFLAGS', ['-O2', '-g'])
+
+		The value must be a list or a tuple
+		"""
+		if isinstance(val, str):
+			val = [val]
+		current_value = self._get_list_value_for_modification(var)
+
+		for x in val:
+			if x not in current_value:
+				current_value.append(x)
+
+	def get_merged_dict(self):
+		"""
+		Compute the merged dictionary from the fusion of self and all its parent
+
+		:rtype: a ConfigSet object
+		"""
+		table_list = []
+		env = self
+		while 1:
+			table_list.insert(0, env.table)
+			try: env = env.parent
+			except AttributeError: break
+		merged_table = {}
+		for table in table_list:
+			merged_table.update(table)
+		return merged_table
+
+	def store(self, filename):
+		"""
+		Write the :py:class:`ConfigSet` data into a file. See :py:meth:`ConfigSet.load` for reading such files.
+
+		:param filename: file to use
+		:type filename: string
+		"""
+		f = None
+		try:
+			f = open(filename, 'w')
+			merged_table = self.get_merged_dict()
+			keys = list(merged_table.keys())
+			keys.sort()
+			for k in keys:
+				if k != 'undo_stack':
+					f.write('%s = %r\n' % (k, merged_table[k]))
+		finally:
+			if f:
+				f.close()
+
+	def load(self, filename):
+		"""
+		Retrieve the :py:class:`ConfigSet` data from a file. See :py:meth:`ConfigSet.store` for writing such files
+
+		:param filename: file to use
+		:type filename: string
+		"""
+		tbl = self.table
+		code = Utils.readf(filename)
+		for m in re_imp.finditer(code):
+			g = m.group
+			tbl[g(2)] = eval(g(3))
+		Logs.debug('env: %s' % str(self.table))
+
+	def update(self, d):
+		"""
+		Dictionary interface: replace values from another dict
+
+		:param d: object to use the value from
+		:type d: dict-like object
+		"""
+		for k, v in d.items():
+			self[k] = v
+
+	def stash(self):
+		"""
+		Store the object state, to provide a kind of transaction support::
+
+			env = ConfigSet()
+			env.stash()
+			try:
+				env.append_value('CFLAGS', '-O3')
+				call_some_method(env)
+			finally:
+				env.revert()
+
+		The history is kept in a stack, and is lost during the serialization by :py:meth:`ConfigSet.store`
+		"""
+		self.undo_stack = self.undo_stack + [self.table]
+		self.table = self.table.copy()
+
+	def revert(self):
+		"""
+		Reverts the object to a previous state. See :py:meth:`ConfigSet.stash`
+		"""
+		self.table = self.undo_stack.pop(-1)
+

File waflib/Configure.py

+#!/usr/bin/env python
+# encoding: utf-8
+# Thomas Nagy, 2005-2010 (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, shlex, sys, time
+from waflib import ConfigSet, Utils, Options, Logs, Context, Build, Errors
+
+try:
+	from urllib import request
+except:
+	from urllib import urlopen
+else:
+	urlopen = request.urlopen
+
+BREAK    = 'break'
+"""In case of a configuration error, break"""
+
+CONTINUE = 'continue'
+"""In case of a configuration error, continue"""
+
+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
+#'''
+
+def download_check(node):
+	"""
+	Hook to check for the tools which are downloaded. Replace with your function if necessary.
+	"""
+	pass
+
+def download_tool(tool, force=False, ctx=None):
+	"""
+	Download a Waf tool from the remote repository defined in :py:const:`waflib.Context.remote_repo`::
+
+		$ waf configure --download
+	"""
+	for x in Utils.to_list(Context.remote_repo):
+		for sub in Utils.to_list(Context.remote_locs):
+			url = '/'.join((x, sub, tool + '.py'))
+			try:
+				web = urlopen(url)
+				try:
+					if web.getcode() != 200:
+						continue
+				except AttributeError:
+					pass
+			except Exception as e:
+				# on python3 urlopen throws an exception
+				# python 2.3 does not have getcode and throws an exception to fail
+				continue
+			else:
+				tmp = ctx.root.make_node(os.sep.join((Context.waf_dir, 'waflib', 'extras', tool + '.py')))
+				tmp.write(web.read())
+				Logs.warn('Downloaded %s from %s' % (tool, url))
+				download_check(tmp)
+				try:
+					module = Context.load_tool(tool)
+				except:
+					Logs.warn('The tool %s from %s is unusable' % (tool, url))
+					try:
+						tmp.delete()
+					except:
+						pass
+					continue
+				return module
+	raise Errors.WafError('Could not load the Waf tool')
+
+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
+
+		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 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', '')
+
+		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)
+
+		now = time.ctime()
+		pyver = sys.hexversion
+		systype = sys.platform
+		args = " ".join(sys.argv)
+		wafver = Context.WAFVERSION
+		abi = Context.ABI
+		self.to_log(conf_template % vars())
+
+		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 (remember to use "update_outputs")')
+		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.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:
+			env.store(Context.run_dir + os.sep + Options.lockfile)
+		if not self.env.NO_LOCK_IN_TOP:
+			env.store(Context.top_dir + os.sep + Options.lockfile)
+		if not self.env.NO_LOCK_IN_OUT:
+			env.store(Context.out_dir + os.sep + 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:
+			env.PREFIX = os.path.abspath(os.path.expanduser(Options.options.prefix))
+		if not env.BINDIR:
+			env.BINDIR = Utils.subst_vars('${PREFIX}/bin', env)
+		if not env.LIBDIR:
+			env.LIBDIR = Utils.subst_vars('${PREFIX}/lib', 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, input, tooldir=None, funs=None, download=True):
+		"""
+		Load Waf tools, which will be imported whenever a build is started.
+
+		:param input: waf tools to import
+		:type input: 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 download: whether to download the tool from the waf repository
+		:type download: bool
+		"""
+
+		tools = Utils.to_list(input)
+		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
+
+			mag = (tool, id(self.env), 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)
+			except ImportError as e:
+				if Options.options.download:
+					module = download_tool(tool, ctx=self)
+					if not module:
+						self.fatal('Could not load the Waf tool %r or download a suitable replacement from the repository (sys.path %r)\n%s' % (tool, sys.path, e))
+				else:
+					self.fatal('Could not load the Waf tool %r from %r (try the --download option?):\n%s' % (tool, sys.path, e))
+			except Exception as e:
+				self.to_log('imp %r (%r & %r)' % (tool, tooldir, funs))
+				self.to_log(Utils.ex_stack())
+				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 = hash((self.hash, node.read('rb')))
+		self.files.append(node.abspath())
+
+	def eval_rules(self, rules):
+		"""
+		Execute the configuration tests. The method :py:meth:`waflib.Configure.ConfigurationContext.err_handler`
+		is used to process the eventual exceptions
+
+		: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 method '%s'." % x)
+			try:
+				f()
+			except Exception as e:
+				ret = self.err_handler(x, e)
+				if ret == BREAK:
+					break
+				elif ret == CONTINUE:
+					continue
+				else:
+					raise
+
+	def err_handler(self, fun, error):
+		"""
+		Error handler for the configuration tests, the default is to let the exception raise
+
+		:param fun: configuration test
+		:type fun: method
+		:param error: exception
+		:type error: exception
+		"""
+		pass
+
+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 = True
+		if 'mandatory' in kw:
+			mandatory = kw['mandatory']
+			del kw['mandatory']
+
+		try:
+			return f(*k, **kw)
+		except Errors.ConfigurationError as e:
+			if mandatory:
+				raise e
+
+	setattr(ConfigurationContext, f.__name__, fun)
+	setattr(Build.BuildContext, f.__name__, fun)
+	return f
+
+@conf
+def add_os_flags(self, var, dest=None):
+	"""
+	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
+	"""
+	# do not use 'get' to make certain the variable is not defined
+	try: self.env.append_value(dest or var, shlex.split(self.environ[var]))
+	except KeyError: pass
+
+@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) and cmd.find(' '):
+		try:
+			os.stat(cmd)
+		except OSError:
+			return shlex.split(cmd)
+		else:
+			return [cmd]
+	return cmd
+
+@conf
+def check_waf_version(self, mini='1.6.0', maxi='1.7.0'):
+	"""
+	check for the waf version
+
+	Versions should be supplied as hex. 0x01000000 means 1.0.0,
+	0x010408 means 1.4.8, etc.
+
+	: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)))
+	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')
+
+@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 occurrence filename or '' if filename could not be found
+	"""
+	for n in Utils.to_list(filename):
+		for d in Utils.to_list(path_list):
+			p = 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 finding a specific program version, for example::
+
+		$ VALAC=/usr/bin/valac_test 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], by default use filename.upper()
+	:type var: string
+	:param ext: list of extensions for the binary (do not add an extension for portability)
+	:type ext: list of string
+	"""
+
+	exts = kw.get('exts', Options.platform == 'win32' and '.exe,.com,.bat,.cmd' or ',.sh,.pl,.py')
+
+	environ = kw.get('environ', os.environ)
+
+	ret = ''
+	filename = Utils.to_list(filename)
+
+	var = kw.get('var', '')
+	if not var:
+		var = filename[0].upper()
+
+	if self.env[var]:
+		ret = self.env[var]
+	elif var in environ:
+		ret = environ[var]
+
+	path_list = kw.get('path_list', '')
+	if not ret:
+		if path_list: