1#!/usr/bin/env python
2# encoding: utf-8
3# Thomas Nagy, 2005-2018 (ita)
4
5"Module called for configuring, compiling and installing targets"
6
7from __future__ import with_statement
8
9import os, shlex, shutil, traceback, errno, sys, stat
10from waflib import Utils, Configure, Logs, Options, ConfigSet, Context, Errors, Build, Node
11
12build_dir_override = None
13
14no_climb_commands = ['configure']
15
16default_cmd = "build"
17
18def waf_entry_point(current_directory, version, wafdir):
19	"""
20	This is the main entry point, all Waf execution starts here.
21
22	:param current_directory: absolute path representing the current directory
23	:type current_directory: string
24	:param version: version number
25	:type version: string
26	:param wafdir: absolute path representing the directory of the waf library
27	:type wafdir: string
28	"""
29	Logs.init_log()
30
31	if Context.WAFVERSION != version:
32		Logs.error('Waf script %r and library %r do not match (directory %r)', version, Context.WAFVERSION, wafdir)
33		sys.exit(1)
34
35	# Store current directory before any chdir
36	Context.waf_dir = wafdir
37	Context.run_dir = Context.launch_dir = current_directory
38	start_dir = current_directory
39	no_climb = os.environ.get('NOCLIMB')
40
41	if len(sys.argv) > 1:
42		# os.path.join handles absolute paths
43		# if sys.argv[1] is not an absolute path, then it is relative to the current working directory
44		potential_wscript = os.path.join(current_directory, sys.argv[1])
45		if os.path.basename(potential_wscript) == Context.WSCRIPT_FILE and os.path.isfile(potential_wscript):
46			# need to explicitly normalize the path, as it may contain extra '/.'
47			path = os.path.normpath(os.path.dirname(potential_wscript))
48			start_dir = os.path.abspath(path)
49			no_climb = True
50			sys.argv.pop(1)
51
52	ctx = Context.create_context('options')
53	(options, commands, env) = ctx.parse_cmd_args(allow_unknown=True)
54	if options.top:
55		start_dir = Context.run_dir = Context.top_dir = options.top
56		no_climb = True
57	if options.out:
58		Context.out_dir = options.out
59
60	# if 'configure' is in the commands, do not search any further
61	if not no_climb:
62		for k in no_climb_commands:
63			for y in commands:
64				if y.startswith(k):
65					no_climb = True
66					break
67
68	# try to find a lock file (if the project was configured)
69	# at the same time, store the first wscript file seen
70	cur = start_dir
71	while cur:
72		try:
73			lst = os.listdir(cur)
74		except OSError:
75			lst = []
76			Logs.error('Directory %r is unreadable!', cur)
77		if Options.lockfile in lst:
78			env = ConfigSet.ConfigSet()
79			try:
80				env.load(os.path.join(cur, Options.lockfile))
81				ino = os.stat(cur)[stat.ST_INO]
82			except EnvironmentError:
83				pass
84			else:
85				# check if the folder was not moved
86				for x in (env.run_dir, env.top_dir, env.out_dir):
87					if not x:
88						continue
89					if Utils.is_win32:
90						if cur == x:
91							load = True
92							break
93					else:
94						# if the filesystem features symlinks, compare the inode numbers
95						try:
96							ino2 = os.stat(x)[stat.ST_INO]
97						except OSError:
98							pass
99						else:
100							if ino == ino2:
101								load = True
102								break
103				else:
104					Logs.warn('invalid lock file in %s', cur)
105					load = False
106
107				if load:
108					Context.run_dir = env.run_dir
109					Context.top_dir = env.top_dir
110					Context.out_dir = env.out_dir
111					break
112
113		if not Context.run_dir:
114			if Context.WSCRIPT_FILE in lst:
115				Context.run_dir = cur
116
117		next = os.path.dirname(cur)
118		if next == cur:
119			break
120		cur = next
121
122		if no_climb:
123			break
124
125	wscript = os.path.normpath(os.path.join(Context.run_dir, Context.WSCRIPT_FILE))
126	if not os.path.exists(wscript):
127		if options.whelp:
128			Logs.warn('These are the generic options (no wscript/project found)')
129			ctx.parser.print_help()
130			sys.exit(0)
131		Logs.error('Waf: Run from a folder containing a %r file (or try -h for the generic options)', Context.WSCRIPT_FILE)
132		sys.exit(1)
133
134	try:
135		os.chdir(Context.run_dir)
136	except OSError:
137		Logs.error('Waf: The folder %r is unreadable', Context.run_dir)
138		sys.exit(1)
139
140	try:
141		set_main_module(wscript)
142	except Errors.WafError as e:
143		Logs.pprint('RED', e.verbose_msg)
144		Logs.error(str(e))
145		sys.exit(1)
146	except Exception as e:
147		Logs.error('Waf: The wscript in %r is unreadable', Context.run_dir)
148		traceback.print_exc(file=sys.stdout)
149		sys.exit(2)
150
151	if options.profile:
152		import cProfile, pstats
153		cProfile.runctx('from waflib import Scripting; Scripting.run_commands()', {}, {}, 'profi.txt')
154		p = pstats.Stats('profi.txt')
155		p.sort_stats('time').print_stats(75) # or 'cumulative'
156	else:
157		try:
158			try:
159				run_commands()
160			except:
161				if options.pdb:
162					import pdb
163					type, value, tb = sys.exc_info()
164					traceback.print_exc()
165					pdb.post_mortem(tb)
166				else:
167					raise
168		except Errors.WafError as e:
169			if Logs.verbose > 1:
170				Logs.pprint('RED', e.verbose_msg)
171			Logs.error(e.msg)
172			sys.exit(1)
173		except SystemExit:
174			raise
175		except Exception as e:
176			traceback.print_exc(file=sys.stdout)
177			sys.exit(2)
178		except KeyboardInterrupt:
179			Logs.pprint('RED', 'Interrupted')
180			sys.exit(68)
181
182def set_main_module(file_path):
183	"""
184	Read the main wscript file into :py:const:`waflib.Context.Context.g_module` and
185	bind default functions such as ``init``, ``dist``, ``distclean`` if not defined.
186	Called by :py:func:`waflib.Scripting.waf_entry_point` during the initialization.
187
188	:param file_path: absolute path representing the top-level wscript file
189	:type file_path: string
190	"""
191	Context.g_module = Context.load_module(file_path)
192	Context.g_module.root_path = file_path
193
194	# note: to register the module globally, use the following:
195	# sys.modules['wscript_main'] = g_module
196
197	def set_def(obj):
198		name = obj.__name__
199		if not name in Context.g_module.__dict__:
200			setattr(Context.g_module, name, obj)
201	for k in (dist, distclean, distcheck):
202		set_def(k)
203	# add dummy init and shutdown functions if they're not defined
204	if not 'init' in Context.g_module.__dict__:
205		Context.g_module.init = Utils.nada
206	if not 'shutdown' in Context.g_module.__dict__:
207		Context.g_module.shutdown = Utils.nada
208	if not 'options' in Context.g_module.__dict__:
209		Context.g_module.options = Utils.nada
210
211def parse_options():
212	"""
213	Parses the command-line options and initialize the logging system.
214	Called by :py:func:`waflib.Scripting.waf_entry_point` during the initialization.
215	"""
216	ctx = Context.create_context('options')
217	ctx.execute()
218	if not Options.commands:
219		if isinstance(default_cmd, list):
220			Options.commands.extend(default_cmd)
221		else:
222			Options.commands.append(default_cmd)
223	if Options.options.whelp:
224		ctx.parser.print_help()
225		sys.exit(0)
226
227def run_command(cmd_name):
228	"""
229	Executes a single Waf command. Called by :py:func:`waflib.Scripting.run_commands`.
230
231	:param cmd_name: command to execute, like ``build``
232	:type cmd_name: string
233	"""
234	ctx = Context.create_context(cmd_name)
235	ctx.log_timer = Utils.Timer()
236	ctx.options = Options.options # provided for convenience
237	ctx.cmd = cmd_name
238	try:
239		ctx.execute()
240	finally:
241		# Issue 1374
242		ctx.finalize()
243	return ctx
244
245def run_commands():
246	"""
247	Execute the Waf commands that were given on the command-line, and the other options
248	Called by :py:func:`waflib.Scripting.waf_entry_point` during the initialization, and executed
249	after :py:func:`waflib.Scripting.parse_options`.
250	"""
251	parse_options()
252	run_command('init')
253	while Options.commands:
254		cmd_name = Options.commands.pop(0)
255		ctx = run_command(cmd_name)
256		Logs.info('%r finished successfully (%s)', cmd_name, ctx.log_timer)
257	run_command('shutdown')
258
259###########################################################################################
260
261def distclean_dir(dirname):
262	"""
263	Distclean function called in the particular case when::
264
265		top == out
266
267	:param dirname: absolute path of the folder to clean
268	:type dirname: string
269	"""
270	for (root, dirs, files) in os.walk(dirname):
271		for f in files:
272			if f.endswith(('.o', '.moc', '.exe')):
273				fname = os.path.join(root, f)
274				try:
275					os.remove(fname)
276				except OSError:
277					Logs.warn('Could not remove %r', fname)
278
279	for x in (Context.DBFILE, 'config.log'):
280		try:
281			os.remove(x)
282		except OSError:
283			pass
284
285	try:
286		shutil.rmtree(Build.CACHE_DIR)
287	except OSError:
288		pass
289
290def distclean(ctx):
291	'''removes build folders and data'''
292
293	def remove_and_log(k, fun):
294		try:
295			fun(k)
296		except EnvironmentError as e:
297			if e.errno != errno.ENOENT:
298				Logs.warn('Could not remove %r', k)
299
300	# remove waf cache folders on the top-level
301	if not Options.commands:
302		for k in os.listdir('.'):
303			for x in '.waf-2 waf-2 .waf3-2 waf3-2'.split():
304				if k.startswith(x):
305					remove_and_log(k, shutil.rmtree)
306
307	# remove a build folder, if any
308	cur = '.'
309	if os.environ.get('NO_LOCK_IN_TOP') or ctx.options.no_lock_in_top:
310		cur = ctx.options.out
311
312	try:
313		lst = os.listdir(cur)
314	except OSError:
315		Logs.warn('Could not read %r', cur)
316		return
317
318	if Options.lockfile in lst:
319		f = os.path.join(cur, Options.lockfile)
320		try:
321			env = ConfigSet.ConfigSet(f)
322		except EnvironmentError:
323			Logs.warn('Could not read %r', f)
324			return
325
326		if not env.out_dir or not env.top_dir:
327			Logs.warn('Invalid lock file %r', f)
328			return
329
330		if env.out_dir == env.top_dir:
331			distclean_dir(env.out_dir)
332		else:
333			remove_and_log(env.out_dir, shutil.rmtree)
334
335		env_dirs = [env.out_dir]
336		if not (os.environ.get('NO_LOCK_IN_TOP') or ctx.options.no_lock_in_top):
337			env_dirs.append(env.top_dir)
338		if not (os.environ.get('NO_LOCK_IN_RUN') or ctx.options.no_lock_in_run):
339			env_dirs.append(env.run_dir)
340		for k in env_dirs:
341			p = os.path.join(k, Options.lockfile)
342			remove_and_log(p, os.remove)
343
344class Dist(Context.Context):
345	'''creates an archive containing the project source code'''
346	cmd = 'dist'
347	fun = 'dist'
348	algo = 'tar.bz2'
349	ext_algo = {}
350
351	def execute(self):
352		"""
353		See :py:func:`waflib.Context.Context.execute`
354		"""
355		self.recurse([os.path.dirname(Context.g_module.root_path)])
356		self.archive()
357
358	def archive(self):
359		"""
360		Creates the source archive.
361		"""
362		import tarfile
363
364		arch_name = self.get_arch_name()
365
366		try:
367			self.base_path
368		except AttributeError:
369			self.base_path = self.path
370
371		node = self.base_path.make_node(arch_name)
372		try:
373			node.delete()
374		except OSError:
375			pass
376
377		files = self.get_files()
378
379		if self.algo.startswith('tar.'):
380			tar = tarfile.open(node.abspath(), 'w:' + self.algo.replace('tar.', ''))
381
382			for x in files:
383				self.add_tar_file(x, tar)
384			tar.close()
385		elif self.algo == 'zip':
386			import zipfile
387			zip = zipfile.ZipFile(node.abspath(), 'w', compression=zipfile.ZIP_DEFLATED)
388
389			for x in files:
390				archive_name = self.get_base_name() + '/' + x.path_from(self.base_path)
391				zip.write(x.abspath(), archive_name, zipfile.ZIP_DEFLATED)
392			zip.close()
393		else:
394			self.fatal('Valid algo types are tar.bz2, tar.gz, tar.xz or zip')
395
396		try:
397			from hashlib import sha256
398		except ImportError:
399			digest = ''
400		else:
401			digest = ' (sha256=%r)' % sha256(node.read(flags='rb')).hexdigest()
402
403		Logs.info('New archive created: %s%s', self.arch_name, digest)
404
405	def get_tar_path(self, node):
406		"""
407		Return the path to use for a node in the tar archive, the purpose of this
408		is to let subclases resolve symbolic links or to change file names
409
410		:return: absolute path
411		:rtype: string
412		"""
413		return node.abspath()
414
415	def add_tar_file(self, x, tar):
416		"""
417		Adds a file to the tar archive. Symlinks are not verified.
418
419		:param x: file path
420		:param tar: tar file object
421		"""
422		p = self.get_tar_path(x)
423		tinfo = tar.gettarinfo(name=p, arcname=self.get_tar_prefix() + '/' + x.path_from(self.base_path))
424		tinfo.uid   = 0
425		tinfo.gid   = 0
426		tinfo.uname = 'root'
427		tinfo.gname = 'root'
428
429		if os.path.isfile(p):
430			with open(p, 'rb') as f:
431				tar.addfile(tinfo, fileobj=f)
432		else:
433			tar.addfile(tinfo)
434
435	def get_tar_prefix(self):
436		"""
437		Returns the base path for files added into the archive tar file
438
439		:rtype: string
440		"""
441		try:
442			return self.tar_prefix
443		except AttributeError:
444			return self.get_base_name()
445
446	def get_arch_name(self):
447		"""
448		Returns the archive file name.
449		Set the attribute *arch_name* to change the default value::
450
451			def dist(ctx):
452				ctx.arch_name = 'ctx.tar.bz2'
453
454		:rtype: string
455		"""
456		try:
457			self.arch_name
458		except AttributeError:
459			self.arch_name = self.get_base_name() + '.' + self.ext_algo.get(self.algo, self.algo)
460		return self.arch_name
461
462	def get_base_name(self):
463		"""
464		Returns the default name of the main directory in the archive, which is set to *appname-version*.
465		Set the attribute *base_name* to change the default value::
466
467			def dist(ctx):
468				ctx.base_name = 'files'
469
470		:rtype: string
471		"""
472		try:
473			self.base_name
474		except AttributeError:
475			appname = getattr(Context.g_module, Context.APPNAME, 'noname')
476			version = getattr(Context.g_module, Context.VERSION, '1.0')
477			self.base_name = appname + '-' + version
478		return self.base_name
479
480	def get_excl(self):
481		"""
482		Returns the patterns to exclude for finding the files in the top-level directory.
483		Set the attribute *excl* to change the default value::
484
485			def dist(ctx):
486				ctx.excl = 'build **/*.o **/*.class'
487
488		:rtype: string
489		"""
490		try:
491			return self.excl
492		except AttributeError:
493			self.excl = Node.exclude_regs + ' **/waf-2.* **/.waf-2.* **/waf3-2.* **/.waf3-2.* **/*~ **/*.rej **/*.orig **/*.pyc **/*.pyo **/*.bak **/*.swp **/.lock-w*'
494			if Context.out_dir:
495				nd = self.root.find_node(Context.out_dir)
496				if nd:
497					self.excl += ' ' + nd.path_from(self.base_path)
498			return self.excl
499
500	def get_files(self):
501		"""
502		Files to package are searched automatically by :py:func:`waflib.Node.Node.ant_glob`.
503		Set *files* to prevent this behaviour::
504
505			def dist(ctx):
506				ctx.files = ctx.path.find_node('wscript')
507
508		Files are also searched from the directory 'base_path', to change it, set::
509
510			def dist(ctx):
511				ctx.base_path = path
512
513		:rtype: list of :py:class:`waflib.Node.Node`
514		"""
515		try:
516			files = self.files
517		except AttributeError:
518			files = self.base_path.ant_glob('**/*', excl=self.get_excl())
519		return files
520
521def dist(ctx):
522	'''makes a tarball for redistributing the sources'''
523	pass
524
525class DistCheck(Dist):
526	"""creates an archive with dist, then tries to build it"""
527	fun = 'distcheck'
528	cmd = 'distcheck'
529
530	def execute(self):
531		"""
532		See :py:func:`waflib.Context.Context.execute`
533		"""
534		self.recurse([os.path.dirname(Context.g_module.root_path)])
535		self.archive()
536		self.check()
537
538	def make_distcheck_cmd(self, tmpdir):
539		cfg = []
540		if Options.options.distcheck_args:
541			cfg = shlex.split(Options.options.distcheck_args)
542		else:
543			cfg = [x for x in sys.argv if x.startswith('-')]
544		cmd = [sys.executable, sys.argv[0], 'configure', 'build', 'install', 'uninstall', '--destdir=' + tmpdir] + cfg
545		return cmd
546
547	def check(self):
548		"""
549		Creates the archive, uncompresses it and tries to build the project
550		"""
551		import tempfile, tarfile
552
553		with tarfile.open(self.get_arch_name()) as t:
554			for x in t:
555				t.extract(x)
556
557		instdir = tempfile.mkdtemp('.inst', self.get_base_name())
558		cmd = self.make_distcheck_cmd(instdir)
559		ret = Utils.subprocess.Popen(cmd, cwd=self.get_base_name()).wait()
560		if ret:
561			raise Errors.WafError('distcheck failed with code %r' % ret)
562
563		if os.path.exists(instdir):
564			raise Errors.WafError('distcheck succeeded, but files were left in %s' % instdir)
565
566		shutil.rmtree(self.get_base_name())
567
568
569def distcheck(ctx):
570	'''checks if the project compiles (tarball from 'dist')'''
571	pass
572
573def autoconfigure(execute_method):
574	"""
575	Decorator that enables context commands to run *configure* as needed.
576	"""
577	def execute(self):
578		"""
579		Wraps :py:func:`waflib.Context.Context.execute` on the context class
580		"""
581		if not Configure.autoconfig:
582			return execute_method(self)
583
584		env = ConfigSet.ConfigSet()
585		do_config = False
586		try:
587			env.load(os.path.join(Context.top_dir, Options.lockfile))
588		except EnvironmentError:
589			Logs.warn('Configuring the project')
590			do_config = True
591		else:
592			if env.run_dir != Context.run_dir:
593				do_config = True
594			else:
595				h = 0
596				for f in env.files:
597					try:
598						h = Utils.h_list((h, Utils.readf(f, 'rb')))
599					except EnvironmentError:
600						do_config = True
601						break
602				else:
603					do_config = h != env.hash
604
605		if do_config:
606			cmd = env.config_cmd or 'configure'
607			if Configure.autoconfig == 'clobber':
608				tmp = Options.options.__dict__
609				launch_dir_tmp = Context.launch_dir
610				if env.options:
611					Options.options.__dict__ = env.options
612				Context.launch_dir = env.launch_dir
613				try:
614					run_command(cmd)
615				finally:
616					Options.options.__dict__ = tmp
617					Context.launch_dir = launch_dir_tmp
618			else:
619				run_command(cmd)
620			run_command(self.cmd)
621		else:
622			return execute_method(self)
623	return execute
624Build.BuildContext.execute = autoconfigure(Build.BuildContext.execute)
625
626