waf

FORK: waf with some random patches
git clone https://git.neptards.moe/neptards/waf.git
Log | Files | Refs | README

Scripting.py (16636B)


      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 
      7 from __future__ import with_statement
      8 
      9 import os, shlex, shutil, traceback, errno, sys, stat
     10 from waflib import Utils, Configure, Logs, Options, ConfigSet, Context, Errors, Build, Node
     11 
     12 build_dir_override = None
     13 
     14 no_climb_commands = ['configure']
     15 
     16 default_cmd = "build"
     17 
     18 def 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 
    182 def 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 
    211 def 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 
    227 def 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 
    245 def 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 
    261 def 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 
    290 def 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 
    344 class 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 
    521 def dist(ctx):
    522 	'''makes a tarball for redistributing the sources'''
    523 	pass
    524 
    525 class 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 
    569 def distcheck(ctx):
    570 	'''checks if the project compiles (tarball from 'dist')'''
    571 	pass
    572 
    573 def 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
    624 Build.BuildContext.execute = autoconfigure(Build.BuildContext.execute)
    625