Index: waflib/Task.py
===================================================================
--- waflib/Task.py	(revision 0fa325b040d442d9b1c268f55d85c2879fff77f1)
+++ waflib/Task.py	(revision 0fa325b040d442d9b1c268f55d85c2879fff77f1)
@@ -0,0 +1,677 @@
+#! /usr/bin/env python
+# encoding: utf-8
+# WARNING! Do not edit! http://waf.googlecode.com/git/docs/wafbook/single.html#_obtaining_the_waf_file
+
+import os,shutil,re,tempfile
+from waflib import Utils,Logs,Errors
+NOT_RUN=0
+MISSING=1
+CRASHED=2
+EXCEPTION=3
+SKIPPED=8
+SUCCESS=9
+ASK_LATER=-1
+SKIP_ME=-2
+RUN_ME=-3
+COMPILE_TEMPLATE_SHELL='''
+def f(tsk):
+	env = tsk.env
+	gen = tsk.generator
+	bld = gen.bld
+	wd = getattr(tsk, 'cwd', None)
+	p = env.get_flat
+	tsk.last_cmd = cmd = \'\'\' %s \'\'\' % s
+	return tsk.exec_command(cmd, cwd=wd, env=env.env or None)
+'''
+COMPILE_TEMPLATE_NOSHELL='''
+def f(tsk):
+	env = tsk.env
+	gen = tsk.generator
+	bld = gen.bld
+	wd = getattr(tsk, 'cwd', None)
+	def to_list(xx):
+		if isinstance(xx, str): return [xx]
+		return xx
+	tsk.last_cmd = lst = []
+	%s
+	lst = [x for x in lst if x]
+	return tsk.exec_command(lst, cwd=wd, env=env.env or None)
+'''
+def cache_outputs(cls):
+	m1=cls.run
+	def run(self):
+		bld=self.generator.bld
+		if bld.cache_global and not bld.nocache:
+			if self.can_retrieve_cache():
+				return 0
+		return m1(self)
+	cls.run=run
+	m2=cls.post_run
+	def post_run(self):
+		bld=self.generator.bld
+		ret=m2(self)
+		if bld.cache_global and not bld.nocache:
+			self.put_files_cache()
+		return ret
+	cls.post_run=post_run
+	return cls
+classes={}
+class store_task_type(type):
+	def __init__(cls,name,bases,dict):
+		super(store_task_type,cls).__init__(name,bases,dict)
+		name=cls.__name__
+		if name.endswith('_task'):
+			name=name.replace('_task','')
+		if name!='evil'and name!='TaskBase':
+			global classes
+			if getattr(cls,'run_str',None):
+				(f,dvars)=compile_fun(cls.run_str,cls.shell)
+				cls.hcode=cls.run_str
+				cls.run_str=None
+				cls.run=f
+				cls.vars=list(set(cls.vars+dvars))
+				cls.vars.sort()
+			elif getattr(cls,'run',None)and not'hcode'in cls.__dict__:
+				cls.hcode=Utils.h_fun(cls.run)
+			if not getattr(cls,'nocache',None):
+				cls=cache_outputs(cls)
+			getattr(cls,'register',classes)[name]=cls
+evil=store_task_type('evil',(object,),{})
+class TaskBase(evil):
+	color='GREEN'
+	ext_in=[]
+	ext_out=[]
+	before=[]
+	after=[]
+	hcode=''
+	def __init__(self,*k,**kw):
+		self.hasrun=NOT_RUN
+		try:
+			self.generator=kw['generator']
+		except KeyError:
+			self.generator=self
+	def __repr__(self):
+		return'\n\t{task %r: %s %s}'%(self.__class__.__name__,id(self),str(getattr(self,'fun','')))
+	def __str__(self):
+		if hasattr(self,'fun'):
+			return'executing: %s\n'%self.fun.__name__
+		return self.__class__.__name__+'\n'
+	def __hash__(self):
+		return id(self)
+	def exec_command(self,cmd,**kw):
+		bld=self.generator.bld
+		try:
+			if not kw.get('cwd',None):
+				kw['cwd']=bld.cwd
+		except AttributeError:
+			bld.cwd=kw['cwd']=bld.variant_dir
+		return bld.exec_command(cmd,**kw)
+	def runnable_status(self):
+		return RUN_ME
+	def process(self):
+		m=self.master
+		if m.stop:
+			m.out.put(self)
+			return
+		try:
+			del self.generator.bld.task_sigs[self.uid()]
+		except KeyError:
+			pass
+		try:
+			self.generator.bld.returned_tasks.append(self)
+			self.log_display(self.generator.bld)
+			ret=self.run()
+		except Exception:
+			self.err_msg=Utils.ex_stack()
+			self.hasrun=EXCEPTION
+			m.error_handler(self)
+			m.out.put(self)
+			return
+		if ret:
+			self.err_code=ret
+			self.hasrun=CRASHED
+		else:
+			try:
+				self.post_run()
+			except Errors.WafError:
+				pass
+			except Exception:
+				self.err_msg=Utils.ex_stack()
+				self.hasrun=EXCEPTION
+			else:
+				self.hasrun=SUCCESS
+		if self.hasrun!=SUCCESS:
+			m.error_handler(self)
+		m.out.put(self)
+	def run(self):
+		if hasattr(self,'fun'):
+			return self.fun(self)
+		return 0
+	def post_run(self):
+		pass
+	def log_display(self,bld):
+		bld.to_log(self.display())
+	def display(self):
+		col1=Logs.colors(self.color)
+		col2=Logs.colors.NORMAL
+		master=self.master
+		def cur():
+			tmp=-1
+			if hasattr(master,'ready'):
+				tmp-=master.ready.qsize()
+			return master.processed+tmp
+		if self.generator.bld.progress_bar==1:
+			return self.generator.bld.progress_line(cur(),master.total,col1,col2)
+		if self.generator.bld.progress_bar==2:
+			ela=str(self.generator.bld.timer)
+			try:
+				ins=','.join([n.name for n in self.inputs])
+			except AttributeError:
+				ins=''
+			try:
+				outs=','.join([n.name for n in self.outputs])
+			except AttributeError:
+				outs=''
+			return'|Total %s|Current %s|Inputs %s|Outputs %s|Time %s|\n'%(master.total,cur(),ins,outs,ela)
+		s=str(self)
+		if not s:
+			return None
+		total=master.total
+		n=len(str(total))
+		fs='[%%%dd/%%%dd] %%s%%s%%s'%(n,n)
+		return fs%(cur(),total,col1,s,col2)
+	def attr(self,att,default=None):
+		ret=getattr(self,att,self)
+		if ret is self:return getattr(self.__class__,att,default)
+		return ret
+	def hash_constraints(self):
+		cls=self.__class__
+		tup=(str(cls.before),str(cls.after),str(cls.ext_in),str(cls.ext_out),cls.__name__,cls.hcode)
+		h=hash(tup)
+		return h
+	def format_error(self):
+		msg=getattr(self,'last_cmd','')
+		name=getattr(self.generator,'name','')
+		if getattr(self,"err_msg",None):
+			return self.err_msg
+		elif not self.hasrun:
+			return'task in %r was not executed for some reason: %r'%(name,self)
+		elif self.hasrun==CRASHED:
+			try:
+				return' -> task in %r failed (exit status %r): %r\n%r'%(name,self.err_code,self,msg)
+			except AttributeError:
+				return' -> task in %r failed: %r\n%r'%(name,self,msg)
+		elif self.hasrun==MISSING:
+			return' -> missing files in %r: %r\n%r'%(name,self,msg)
+		else:
+			return'invalid status for task in %r: %r'%(name,self.hasrun)
+	def colon(self,var1,var2):
+		tmp=self.env[var1]
+		if isinstance(var2,str):
+			it=self.env[var2]
+		else:
+			it=var2
+		if isinstance(tmp,str):
+			return[tmp%x for x in it]
+		else:
+			if Logs.verbose and not tmp and it:
+				Logs.warn('Missing env variable %r for task %r (generator %r)'%(var1,self,self.generator))
+			lst=[]
+			for y in it:
+				lst.extend(tmp)
+				lst.append(y)
+			return lst
+class Task(TaskBase):
+	vars=[]
+	shell=False
+	def __init__(self,*k,**kw):
+		TaskBase.__init__(self,*k,**kw)
+		self.env=kw['env']
+		self.inputs=[]
+		self.outputs=[]
+		self.dep_nodes=[]
+		self.run_after=set([])
+	def __str__(self):
+		env=self.env
+		src_str=' '.join([a.nice_path()for a in self.inputs])
+		tgt_str=' '.join([a.nice_path()for a in self.outputs])
+		if self.outputs:sep=' -> '
+		else:sep=''
+		return'%s: %s%s%s\n'%(self.__class__.__name__.replace('_task',''),src_str,sep,tgt_str)
+	def __repr__(self):
+		try:
+			ins=",".join([x.name for x in self.inputs])
+			outs=",".join([x.name for x in self.outputs])
+		except AttributeError:
+			ins=",".join([str(x)for x in self.inputs])
+			outs=",".join([str(x)for x in self.outputs])
+		return"".join(['\n\t{task %r: '%id(self),self.__class__.__name__," ",ins," -> ",outs,'}'])
+	def uid(self):
+		try:
+			return self.uid_
+		except AttributeError:
+			m=Utils.md5()
+			up=m.update
+			up(self.__class__.__name__)
+			for x in self.inputs+self.outputs:
+				up(x.abspath())
+			self.uid_=m.digest()
+			return self.uid_
+	def set_inputs(self,inp):
+		if isinstance(inp,list):self.inputs+=inp
+		else:self.inputs.append(inp)
+	def set_outputs(self,out):
+		if isinstance(out,list):self.outputs+=out
+		else:self.outputs.append(out)
+	def set_run_after(self,task):
+		assert isinstance(task,TaskBase)
+		self.run_after.add(task)
+	def signature(self):
+		try:return self.cache_sig
+		except AttributeError:pass
+		self.m=Utils.md5()
+		self.m.update(self.hcode)
+		self.sig_explicit_deps()
+		self.sig_vars()
+		if self.scan:
+			try:
+				self.sig_implicit_deps()
+			except Errors.TaskRescan:
+				return self.signature()
+		ret=self.cache_sig=self.m.digest()
+		return ret
+	def runnable_status(self):
+		for t in self.run_after:
+			if not t.hasrun:
+				return ASK_LATER
+		bld=self.generator.bld
+		try:
+			new_sig=self.signature()
+		except Errors.TaskNotReady:
+			return ASK_LATER
+		key=self.uid()
+		try:
+			prev_sig=bld.task_sigs[key]
+		except KeyError:
+			Logs.debug("task: task %r must run as it was never run before or the task code changed"%self)
+			return RUN_ME
+		for node in self.outputs:
+			try:
+				if node.sig!=new_sig:
+					return RUN_ME
+			except AttributeError:
+				Logs.debug("task: task %r must run as the output nodes do not exist"%self)
+				return RUN_ME
+		if new_sig!=prev_sig:
+			return RUN_ME
+		return SKIP_ME
+	def post_run(self):
+		bld=self.generator.bld
+		sig=self.signature()
+		for node in self.outputs:
+			try:
+				os.stat(node.abspath())
+			except OSError:
+				self.hasrun=MISSING
+				self.err_msg='-> missing file: %r'%node.abspath()
+				raise Errors.WafError(self.err_msg)
+			node.sig=sig
+		bld.task_sigs[self.uid()]=self.cache_sig
+	def sig_explicit_deps(self):
+		bld=self.generator.bld
+		upd=self.m.update
+		for x in self.inputs+self.dep_nodes:
+			try:
+				upd(x.get_bld_sig())
+			except(AttributeError,TypeError):
+				raise Errors.WafError('Missing node signature for %r (required by %r)'%(x,self))
+		if bld.deps_man:
+			additional_deps=bld.deps_man
+			for x in self.inputs+self.outputs:
+				try:
+					d=additional_deps[id(x)]
+				except KeyError:
+					continue
+				for v in d:
+					if isinstance(v,bld.root.__class__):
+						try:
+							v=v.get_bld_sig()
+						except AttributeError:
+							raise Errors.WafError('Missing node signature for %r (required by %r)'%(v,self))
+					elif hasattr(v,'__call__'):
+						v=v()
+					upd(v)
+		return self.m.digest()
+	def sig_vars(self):
+		bld=self.generator.bld
+		env=self.env
+		upd=self.m.update
+		act_sig=bld.hash_env_vars(env,self.__class__.vars)
+		upd(act_sig)
+		dep_vars=getattr(self,'dep_vars',None)
+		if dep_vars:
+			upd(bld.hash_env_vars(env,dep_vars))
+		return self.m.digest()
+	scan=None
+	def sig_implicit_deps(self):
+		bld=self.generator.bld
+		key=self.uid()
+		prev=bld.task_sigs.get((key,'imp'),[])
+		if prev:
+			try:
+				if prev==self.compute_sig_implicit_deps():
+					return prev
+			except Exception:
+				for x in bld.node_deps.get(self.uid(),[]):
+					if x.is_child_of(bld.srcnode):
+						try:
+							os.stat(x.abspath())
+						except OSError:
+							try:
+								del x.parent.children[x.name]
+							except KeyError:
+								pass
+			del bld.task_sigs[(key,'imp')]
+			raise Errors.TaskRescan('rescan')
+		(nodes,names)=self.scan()
+		if Logs.verbose:
+			Logs.debug('deps: scanner for %s returned %s %s'%(str(self),str(nodes),str(names)))
+		bld.node_deps[key]=nodes
+		bld.raw_deps[key]=names
+		self.are_implicit_nodes_ready()
+		try:
+			bld.task_sigs[(key,'imp')]=sig=self.compute_sig_implicit_deps()
+		except Exception:
+			if Logs.verbose:
+				for k in bld.node_deps.get(self.uid(),[]):
+					try:
+						k.get_bld_sig()
+					except Exception:
+						Logs.warn('Missing signature for node %r (may cause rebuilds)'%k)
+		else:
+			return sig
+	def compute_sig_implicit_deps(self):
+		upd=self.m.update
+		bld=self.generator.bld
+		self.are_implicit_nodes_ready()
+		for k in bld.node_deps.get(self.uid(),[]):
+			upd(k.get_bld_sig())
+		return self.m.digest()
+	def are_implicit_nodes_ready(self):
+		bld=self.generator.bld
+		try:
+			cache=bld.dct_implicit_nodes
+		except AttributeError:
+			bld.dct_implicit_nodes=cache={}
+		try:
+			dct=cache[bld.cur]
+		except KeyError:
+			dct=cache[bld.cur]={}
+			for tsk in bld.cur_tasks:
+				for x in tsk.outputs:
+					dct[x]=tsk
+		modified=False
+		for x in bld.node_deps.get(self.uid(),[]):
+			if x in dct:
+				self.run_after.add(dct[x])
+				modified=True
+		if modified:
+			for tsk in self.run_after:
+				if not tsk.hasrun:
+					raise Errors.TaskNotReady('not ready')
+	def can_retrieve_cache(self):
+		if not getattr(self,'outputs',None):
+			return None
+		sig=self.signature()
+		ssig=Utils.to_hex(self.uid())+Utils.to_hex(sig)
+		dname=os.path.join(self.generator.bld.cache_global,ssig)
+		try:
+			t1=os.stat(dname).st_mtime
+		except OSError:
+			return None
+		for node in self.outputs:
+			orig=os.path.join(dname,node.name)
+			try:
+				shutil.copy2(orig,node.abspath())
+				os.utime(orig,None)
+			except(OSError,IOError):
+				Logs.debug('task: failed retrieving file')
+				return None
+		try:
+			t2=os.stat(dname).st_mtime
+		except OSError:
+			return None
+		if t1!=t2:
+			return None
+		for node in self.outputs:
+			node.sig=sig
+			if self.generator.bld.progress_bar<1:
+				self.generator.bld.to_log('restoring from cache %r\n'%node.abspath())
+		self.cached=True
+		return True
+	def put_files_cache(self):
+		if getattr(self,'cached',None):
+			return None
+		if not getattr(self,'outputs',None):
+			return None
+		sig=self.signature()
+		ssig=Utils.to_hex(self.uid())+Utils.to_hex(sig)
+		dname=os.path.join(self.generator.bld.cache_global,ssig)
+		tmpdir=tempfile.mkdtemp(prefix=self.generator.bld.cache_global+os.sep+'waf')
+		try:
+			shutil.rmtree(dname)
+		except Exception:
+			pass
+		try:
+			for node in self.outputs:
+				dest=os.path.join(tmpdir,node.name)
+				shutil.copy2(node.abspath(),dest)
+		except(OSError,IOError):
+			try:
+				shutil.rmtree(tmpdir)
+			except Exception:
+				pass
+		else:
+			try:
+				os.rename(tmpdir,dname)
+			except OSError:
+				try:
+					shutil.rmtree(tmpdir)
+				except Exception:
+					pass
+			else:
+				try:
+					os.chmod(dname,Utils.O755)
+				except Exception:
+					pass
+def is_before(t1,t2):
+	to_list=Utils.to_list
+	for k in to_list(t2.ext_in):
+		if k in to_list(t1.ext_out):
+			return 1
+	if t1.__class__.__name__ in to_list(t2.after):
+		return 1
+	if t2.__class__.__name__ in to_list(t1.before):
+		return 1
+	return 0
+def set_file_constraints(tasks):
+	ins=Utils.defaultdict(set)
+	outs=Utils.defaultdict(set)
+	for x in tasks:
+		for a in getattr(x,'inputs',[])+getattr(x,'dep_nodes',[]):
+			ins[id(a)].add(x)
+		for a in getattr(x,'outputs',[]):
+			outs[id(a)].add(x)
+	links=set(ins.keys()).intersection(outs.keys())
+	for k in links:
+		for a in ins[k]:
+			a.run_after.update(outs[k])
+def set_precedence_constraints(tasks):
+	cstr_groups=Utils.defaultdict(list)
+	for x in tasks:
+		h=x.hash_constraints()
+		cstr_groups[h].append(x)
+	keys=list(cstr_groups.keys())
+	maxi=len(keys)
+	for i in range(maxi):
+		t1=cstr_groups[keys[i]][0]
+		for j in range(i+1,maxi):
+			t2=cstr_groups[keys[j]][0]
+			if is_before(t1,t2):
+				a=i
+				b=j
+			elif is_before(t2,t1):
+				a=j
+				b=i
+			else:
+				continue
+			aval=set(cstr_groups[keys[a]])
+			for x in cstr_groups[keys[b]]:
+				x.run_after.update(aval)
+def funex(c):
+	dc={}
+	exec(c,dc)
+	return dc['f']
+reg_act=re.compile(r"(?P<backslash>\\)|(?P<dollar>\$\$)|(?P<subst>\$\{(?P<var>\w+)(?P<code>.*?)\})",re.M)
+def compile_fun_shell(line):
+	extr=[]
+	def repl(match):
+		g=match.group
+		if g('dollar'):return"$"
+		elif g('backslash'):return'\\\\'
+		elif g('subst'):extr.append((g('var'),g('code')));return"%s"
+		return None
+	line=reg_act.sub(repl,line)or line
+	parm=[]
+	dvars=[]
+	app=parm.append
+	for(var,meth)in extr:
+		if var=='SRC':
+			if meth:app('tsk.inputs%s'%meth)
+			else:app('" ".join([a.path_from(bld.bldnode) for a in tsk.inputs])')
+		elif var=='TGT':
+			if meth:app('tsk.outputs%s'%meth)
+			else:app('" ".join([a.path_from(bld.bldnode) for a in tsk.outputs])')
+		elif meth:
+			if meth.startswith(':'):
+				m=meth[1:]
+				if m=='SRC':
+					m='[a.path_from(bld.bldnode) for a in tsk.inputs]'
+				elif m=='TGT':
+					m='[a.path_from(bld.bldnode) for a in tsk.outputs]'
+				elif m[:3]not in('tsk','gen','bld'):
+					dvars.extend([var,meth[1:]])
+					m='%r'%m
+				app('" ".join(tsk.colon(%r, %s))'%(var,m))
+			else:
+				app('%s%s'%(var,meth))
+		else:
+			if not var in dvars:dvars.append(var)
+			app("p('%s')"%var)
+	if parm:parm="%% (%s) "%(',\n\t\t'.join(parm))
+	else:parm=''
+	c=COMPILE_TEMPLATE_SHELL%(line,parm)
+	Logs.debug('action: %s'%c.strip().splitlines())
+	return(funex(c),dvars)
+def compile_fun_noshell(line):
+	extr=[]
+	def repl(match):
+		g=match.group
+		if g('dollar'):return"$"
+		elif g('subst'):extr.append((g('var'),g('code')));return"<<|@|>>"
+		return None
+	line2=reg_act.sub(repl,line)
+	params=line2.split('<<|@|>>')
+	assert(extr)
+	buf=[]
+	dvars=[]
+	app=buf.append
+	for x in range(len(extr)):
+		params[x]=params[x].strip()
+		if params[x]:
+			app("lst.extend(%r)"%params[x].split())
+		(var,meth)=extr[x]
+		if var=='SRC':
+			if meth:app('lst.append(tsk.inputs%s)'%meth)
+			else:app("lst.extend([a.path_from(bld.bldnode) for a in tsk.inputs])")
+		elif var=='TGT':
+			if meth:app('lst.append(tsk.outputs%s)'%meth)
+			else:app("lst.extend([a.path_from(bld.bldnode) for a in tsk.outputs])")
+		elif meth:
+			if meth.startswith(':'):
+				m=meth[1:]
+				if m=='SRC':
+					m='[a.path_from(bld.bldnode) for a in tsk.inputs]'
+				elif m=='TGT':
+					m='[a.path_from(bld.bldnode) for a in tsk.outputs]'
+				elif m[:3]not in('tsk','gen','bld'):
+					dvars.extend([var,m])
+					m='%r'%m
+				app('lst.extend(tsk.colon(%r, %s))'%(var,m))
+			else:
+				app('lst.extend(gen.to_list(%s%s))'%(var,meth))
+		else:
+			app('lst.extend(to_list(env[%r]))'%var)
+			if not var in dvars:dvars.append(var)
+	if extr:
+		if params[-1]:
+			app("lst.extend(%r)"%params[-1].split())
+	fun=COMPILE_TEMPLATE_NOSHELL%"\n\t".join(buf)
+	Logs.debug('action: %s'%fun.strip().splitlines())
+	return(funex(fun),dvars)
+def compile_fun(line,shell=False):
+	if line.find('<')>0 or line.find('>')>0 or line.find('&&')>0:
+		shell=True
+	if shell:
+		return compile_fun_shell(line)
+	else:
+		return compile_fun_noshell(line)
+def task_factory(name,func=None,vars=None,color='GREEN',ext_in=[],ext_out=[],before=[],after=[],shell=False,scan=None):
+	params={'vars':vars or[],'color':color,'name':name,'ext_in':Utils.to_list(ext_in),'ext_out':Utils.to_list(ext_out),'before':Utils.to_list(before),'after':Utils.to_list(after),'shell':shell,'scan':scan,}
+	if isinstance(func,str):
+		params['run_str']=func
+	else:
+		params['run']=func
+	cls=type(Task)(name,(Task,),params)
+	global classes
+	classes[name]=cls
+	return cls
+def always_run(cls):
+	old=cls.runnable_status
+	def always(self):
+		ret=old(self)
+		if ret==SKIP_ME:
+			ret=RUN_ME
+		return ret
+	cls.runnable_status=always
+	return cls
+def update_outputs(cls):
+	old_post_run=cls.post_run
+	def post_run(self):
+		old_post_run(self)
+		for node in self.outputs:
+			node.sig=Utils.h_file(node.abspath())
+			self.generator.bld.task_sigs[node.abspath()]=self.uid()
+	cls.post_run=post_run
+	old_runnable_status=cls.runnable_status
+	def runnable_status(self):
+		status=old_runnable_status(self)
+		if status!=RUN_ME:
+			return status
+		try:
+			bld=self.generator.bld
+			prev_sig=bld.task_sigs[self.uid()]
+			if prev_sig==self.signature():
+				for x in self.outputs:
+					if not x.sig or bld.task_sigs[x.abspath()]!=self.uid():
+						return RUN_ME
+				return SKIP_ME
+		except KeyError:
+			pass
+		except IndexError:
+			pass
+		except AttributeError:
+			pass
+		return RUN_ME
+	cls.runnable_status=runnable_status
+	return cls
