aboutsummaryrefslogtreecommitdiffstats
path: root/waflib/Scripting.py
diff options
context:
space:
mode:
authorDavid Robillard <d@drobilla.net>2018-09-15 14:51:02 +0200
committerDavid Robillard <d@drobilla.net>2018-09-15 14:51:02 +0200
commit8ac954755634d55e584019bb3ea64079b2e918ed (patch)
tree3b523f3759c8c76fb1886f68d0071bce0a33b28c /waflib/Scripting.py
parentc0782cc4eec3e86498a2241eefa571447f747d6d (diff)
parent8c96b17a5393bffee0c521c4872a9fa999048032 (diff)
Merge commit '8c96b17a5393bffee0c521c4872a9fa999048032' as 'waflib'
Diffstat (limited to 'waflib/Scripting.py')
-rw-r--r--waflib/Scripting.py614
1 files changed, 614 insertions, 0 deletions
diff --git a/waflib/Scripting.py b/waflib/Scripting.py
new file mode 100644
index 0000000..749d4f2
--- /dev/null
+++ b/waflib/Scripting.py
@@ -0,0 +1,614 @@
+#!/usr/bin/env python
+# encoding: utf-8
+# Thomas Nagy, 2005-2018 (ita)
+
+"Module called for configuring, compiling and installing targets"
+
+from __future__ import with_statement
+
+import os, shlex, shutil, traceback, errno, sys, stat
+from waflib import Utils, Configure, Logs, Options, ConfigSet, Context, Errors, Build, Node
+
+build_dir_override = None
+
+no_climb_commands = ['configure']
+
+default_cmd = "build"
+
+def waf_entry_point(current_directory, version, wafdir):
+ """
+ This is the main entry point, all Waf execution starts here.
+
+ :param current_directory: absolute path representing the current directory
+ :type current_directory: string
+ :param version: version number
+ :type version: string
+ :param wafdir: absolute path representing the directory of the waf library
+ :type wafdir: string
+ """
+ Logs.init_log()
+
+ if Context.WAFVERSION != version:
+ Logs.error('Waf script %r and library %r do not match (directory %r)', version, Context.WAFVERSION, wafdir)
+ sys.exit(1)
+
+ # Store current directory before any chdir
+ Context.waf_dir = wafdir
+ Context.run_dir = Context.launch_dir = current_directory
+ start_dir = current_directory
+ no_climb = os.environ.get('NOCLIMB')
+
+ if len(sys.argv) > 1:
+ # os.path.join handles absolute paths
+ # if sys.argv[1] is not an absolute path, then it is relative to the current working directory
+ potential_wscript = os.path.join(current_directory, sys.argv[1])
+ if os.path.basename(potential_wscript) == Context.WSCRIPT_FILE and os.path.isfile(potential_wscript):
+ # need to explicitly normalize the path, as it may contain extra '/.'
+ path = os.path.normpath(os.path.dirname(potential_wscript))
+ start_dir = os.path.abspath(path)
+ no_climb = True
+ sys.argv.pop(1)
+
+ ctx = Context.create_context('options')
+ (options, commands, env) = ctx.parse_cmd_args(allow_unknown=True)
+ if options.top:
+ start_dir = Context.run_dir = Context.top_dir = options.top
+ no_climb = True
+ if options.out:
+ Context.out_dir = options.out
+
+ # if 'configure' is in the commands, do not search any further
+ if not no_climb:
+ for k in no_climb_commands:
+ for y in commands:
+ if y.startswith(k):
+ no_climb = True
+ break
+
+ # try to find a lock file (if the project was configured)
+ # at the same time, store the first wscript file seen
+ cur = start_dir
+ while cur:
+ try:
+ lst = os.listdir(cur)
+ except OSError:
+ lst = []
+ Logs.error('Directory %r is unreadable!', cur)
+ if Options.lockfile in lst:
+ env = ConfigSet.ConfigSet()
+ try:
+ env.load(os.path.join(cur, Options.lockfile))
+ ino = os.stat(cur)[stat.ST_INO]
+ except EnvironmentError:
+ pass
+ else:
+ # check if the folder was not moved
+ for x in (env.run_dir, env.top_dir, env.out_dir):
+ if not x:
+ continue
+ if Utils.is_win32:
+ if cur == x:
+ load = True
+ break
+ else:
+ # if the filesystem features symlinks, compare the inode numbers
+ try:
+ ino2 = os.stat(x)[stat.ST_INO]
+ except OSError:
+ pass
+ else:
+ if ino == ino2:
+ load = True
+ break
+ else:
+ Logs.warn('invalid lock file in %s', cur)
+ load = False
+
+ if load:
+ Context.run_dir = env.run_dir
+ Context.top_dir = env.top_dir
+ Context.out_dir = env.out_dir
+ break
+
+ if not Context.run_dir:
+ if Context.WSCRIPT_FILE in lst:
+ Context.run_dir = cur
+
+ next = os.path.dirname(cur)
+ if next == cur:
+ break
+ cur = next
+
+ if no_climb:
+ break
+
+ wscript = os.path.normpath(os.path.join(Context.run_dir, Context.WSCRIPT_FILE))
+ if not os.path.exists(wscript):
+ if options.whelp:
+ Logs.warn('These are the generic options (no wscript/project found)')
+ ctx.parser.print_help()
+ sys.exit(0)
+ Logs.error('Waf: Run from a folder containing a %r file (or try -h for the generic options)', Context.WSCRIPT_FILE)
+ sys.exit(1)
+
+ try:
+ os.chdir(Context.run_dir)
+ except OSError:
+ Logs.error('Waf: The folder %r is unreadable', Context.run_dir)
+ sys.exit(1)
+
+ try:
+ set_main_module(wscript)
+ except Errors.WafError as e:
+ Logs.pprint('RED', e.verbose_msg)
+ Logs.error(str(e))
+ sys.exit(1)
+ except Exception as e:
+ Logs.error('Waf: The wscript in %r is unreadable', Context.run_dir)
+ traceback.print_exc(file=sys.stdout)
+ sys.exit(2)
+
+ if options.profile:
+ import cProfile, pstats
+ cProfile.runctx('from waflib import Scripting; Scripting.run_commands()', {}, {}, 'profi.txt')
+ p = pstats.Stats('profi.txt')
+ p.sort_stats('time').print_stats(75) # or 'cumulative'
+ else:
+ try:
+ try:
+ run_commands()
+ except:
+ if options.pdb:
+ import pdb
+ type, value, tb = sys.exc_info()
+ traceback.print_exc()
+ pdb.post_mortem(tb)
+ else:
+ raise
+ except Errors.WafError as e:
+ if Logs.verbose > 1:
+ Logs.pprint('RED', e.verbose_msg)
+ Logs.error(e.msg)
+ sys.exit(1)
+ except SystemExit:
+ raise
+ except Exception as e:
+ traceback.print_exc(file=sys.stdout)
+ sys.exit(2)
+ except KeyboardInterrupt:
+ Logs.pprint('RED', 'Interrupted')
+ sys.exit(68)
+
+def set_main_module(file_path):
+ """
+ Read the main wscript file into :py:const:`waflib.Context.Context.g_module` and
+ bind default functions such as ``init``, ``dist``, ``distclean`` if not defined.
+ Called by :py:func:`waflib.Scripting.waf_entry_point` during the initialization.
+
+ :param file_path: absolute path representing the top-level wscript file
+ :type file_path: string
+ """
+ Context.g_module = Context.load_module(file_path)
+ Context.g_module.root_path = file_path
+
+ # note: to register the module globally, use the following:
+ # sys.modules['wscript_main'] = g_module
+
+ def set_def(obj):
+ name = obj.__name__
+ if not name in Context.g_module.__dict__:
+ setattr(Context.g_module, name, obj)
+ for k in (dist, distclean, distcheck):
+ set_def(k)
+ # add dummy init and shutdown functions if they're not defined
+ if not 'init' in Context.g_module.__dict__:
+ Context.g_module.init = Utils.nada
+ if not 'shutdown' in Context.g_module.__dict__:
+ Context.g_module.shutdown = Utils.nada
+ if not 'options' in Context.g_module.__dict__:
+ Context.g_module.options = Utils.nada
+
+def parse_options():
+ """
+ Parses the command-line options and initialize the logging system.
+ Called by :py:func:`waflib.Scripting.waf_entry_point` during the initialization.
+ """
+ ctx = Context.create_context('options')
+ ctx.execute()
+ if not Options.commands:
+ Options.commands.append(default_cmd)
+ if Options.options.whelp:
+ ctx.parser.print_help()
+ sys.exit(0)
+
+def run_command(cmd_name):
+ """
+ Executes a single Waf command. Called by :py:func:`waflib.Scripting.run_commands`.
+
+ :param cmd_name: command to execute, like ``build``
+ :type cmd_name: string
+ """
+ ctx = Context.create_context(cmd_name)
+ ctx.log_timer = Utils.Timer()
+ ctx.options = Options.options # provided for convenience
+ ctx.cmd = cmd_name
+ try:
+ ctx.execute()
+ finally:
+ # Issue 1374
+ ctx.finalize()
+ return ctx
+
+def run_commands():
+ """
+ Execute the Waf commands that were given on the command-line, and the other options
+ Called by :py:func:`waflib.Scripting.waf_entry_point` during the initialization, and executed
+ after :py:func:`waflib.Scripting.parse_options`.
+ """
+ parse_options()
+ run_command('init')
+ while Options.commands:
+ cmd_name = Options.commands.pop(0)
+ ctx = run_command(cmd_name)
+ Logs.info('%r finished successfully (%s)', cmd_name, ctx.log_timer)
+ run_command('shutdown')
+
+###########################################################################################
+
+def distclean_dir(dirname):
+ """
+ Distclean function called in the particular case when::
+
+ top == out
+
+ :param dirname: absolute path of the folder to clean
+ :type dirname: string
+ """
+ for (root, dirs, files) in os.walk(dirname):
+ for f in files:
+ if f.endswith(('.o', '.moc', '.exe')):
+ fname = os.path.join(root, f)
+ try:
+ os.remove(fname)
+ except OSError:
+ Logs.warn('Could not remove %r', fname)
+
+ for x in (Context.DBFILE, 'config.log'):
+ try:
+ os.remove(x)
+ except OSError:
+ pass
+
+ try:
+ shutil.rmtree('c4che')
+ except OSError:
+ pass
+
+def distclean(ctx):
+ '''removes build folders and data'''
+
+ def remove_and_log(k, fun):
+ try:
+ fun(k)
+ except EnvironmentError as e:
+ if e.errno != errno.ENOENT:
+ Logs.warn('Could not remove %r', k)
+
+ # remove waf cache folders on the top-level
+ if not Options.commands:
+ for k in os.listdir('.'):
+ for x in '.waf-2 waf-2 .waf3-2 waf3-2'.split():
+ if k.startswith(x):
+ remove_and_log(k, shutil.rmtree)
+
+ # remove a build folder, if any
+ cur = '.'
+ if ctx.options.no_lock_in_top:
+ cur = ctx.options.out
+
+ try:
+ lst = os.listdir(cur)
+ except OSError:
+ Logs.warn('Could not read %r', cur)
+ return
+
+ if Options.lockfile in lst:
+ f = os.path.join(cur, Options.lockfile)
+ try:
+ env = ConfigSet.ConfigSet(f)
+ except EnvironmentError:
+ Logs.warn('Could not read %r', f)
+ return
+
+ if not env.out_dir or not env.top_dir:
+ Logs.warn('Invalid lock file %r', f)
+ return
+
+ if env.out_dir == env.top_dir:
+ distclean_dir(env.out_dir)
+ else:
+ remove_and_log(env.out_dir, shutil.rmtree)
+
+ for k in (env.out_dir, env.top_dir, env.run_dir):
+ p = os.path.join(k, Options.lockfile)
+ remove_and_log(p, os.remove)
+
+class Dist(Context.Context):
+ '''creates an archive containing the project source code'''
+ cmd = 'dist'
+ fun = 'dist'
+ algo = 'tar.bz2'
+ ext_algo = {}
+
+ def execute(self):
+ """
+ See :py:func:`waflib.Context.Context.execute`
+ """
+ self.recurse([os.path.dirname(Context.g_module.root_path)])
+ self.archive()
+
+ def archive(self):
+ """
+ Creates the source archive.
+ """
+ import tarfile
+
+ arch_name = self.get_arch_name()
+
+ try:
+ self.base_path
+ except AttributeError:
+ self.base_path = self.path
+
+ node = self.base_path.make_node(arch_name)
+ try:
+ node.delete()
+ except OSError:
+ pass
+
+ files = self.get_files()
+
+ if self.algo.startswith('tar.'):
+ tar = tarfile.open(node.abspath(), 'w:' + self.algo.replace('tar.', ''))
+
+ for x in files:
+ self.add_tar_file(x, tar)
+ tar.close()
+ elif self.algo == 'zip':
+ import zipfile
+ zip = zipfile.ZipFile(node.abspath(), 'w', compression=zipfile.ZIP_DEFLATED)
+
+ for x in files:
+ archive_name = self.get_base_name() + '/' + x.path_from(self.base_path)
+ zip.write(x.abspath(), archive_name, zipfile.ZIP_DEFLATED)
+ zip.close()
+ else:
+ self.fatal('Valid algo types are tar.bz2, tar.gz, tar.xz or zip')
+
+ try:
+ from hashlib import sha256
+ except ImportError:
+ digest = ''
+ else:
+ digest = ' (sha256=%r)' % sha256(node.read(flags='rb')).hexdigest()
+
+ Logs.info('New archive created: %s%s', self.arch_name, digest)
+
+ def get_tar_path(self, node):
+ """
+ Return the path to use for a node in the tar archive, the purpose of this
+ is to let subclases resolve symbolic links or to change file names
+
+ :return: absolute path
+ :rtype: string
+ """
+ return node.abspath()
+
+ def add_tar_file(self, x, tar):
+ """
+ Adds a file to the tar archive. Symlinks are not verified.
+
+ :param x: file path
+ :param tar: tar file object
+ """
+ p = self.get_tar_path(x)
+ tinfo = tar.gettarinfo(name=p, arcname=self.get_tar_prefix() + '/' + x.path_from(self.base_path))
+ tinfo.uid = 0
+ tinfo.gid = 0
+ tinfo.uname = 'root'
+ tinfo.gname = 'root'
+
+ if os.path.isfile(p):
+ with open(p, 'rb') as f:
+ tar.addfile(tinfo, fileobj=f)
+ else:
+ tar.addfile(tinfo)
+
+ def get_tar_prefix(self):
+ """
+ Returns the base path for files added into the archive tar file
+
+ :rtype: string
+ """
+ try:
+ return self.tar_prefix
+ except AttributeError:
+ return self.get_base_name()
+
+ def get_arch_name(self):
+ """
+ Returns the archive file name.
+ Set the attribute *arch_name* to change the default value::
+
+ def dist(ctx):
+ ctx.arch_name = 'ctx.tar.bz2'
+
+ :rtype: string
+ """
+ try:
+ self.arch_name
+ except AttributeError:
+ self.arch_name = self.get_base_name() + '.' + self.ext_algo.get(self.algo, self.algo)
+ return self.arch_name
+
+ def get_base_name(self):
+ """
+ Returns the default name of the main directory in the archive, which is set to *appname-version*.
+ Set the attribute *base_name* to change the default value::
+
+ def dist(ctx):
+ ctx.base_name = 'files'
+
+ :rtype: string
+ """
+ try:
+ self.base_name
+ except AttributeError:
+ appname = getattr(Context.g_module, Context.APPNAME, 'noname')
+ version = getattr(Context.g_module, Context.VERSION, '1.0')
+ self.base_name = appname + '-' + version
+ return self.base_name
+
+ def get_excl(self):
+ """
+ Returns the patterns to exclude for finding the files in the top-level directory.
+ Set the attribute *excl* to change the default value::
+
+ def dist(ctx):
+ ctx.excl = 'build **/*.o **/*.class'
+
+ :rtype: string
+ """
+ try:
+ return self.excl
+ except AttributeError:
+ self.excl = Node.exclude_regs + ' **/waf-2.* **/.waf-2.* **/waf3-2.* **/.waf3-2.* **/*~ **/*.rej **/*.orig **/*.pyc **/*.pyo **/*.bak **/*.swp **/.lock-w*'
+ if Context.out_dir:
+ nd = self.root.find_node(Context.out_dir)
+ if nd:
+ self.excl += ' ' + nd.path_from(self.base_path)
+ return self.excl
+
+ def get_files(self):
+ """
+ Files to package are searched automatically by :py:func:`waflib.Node.Node.ant_glob`.
+ Set *files* to prevent this behaviour::
+
+ def dist(ctx):
+ ctx.files = ctx.path.find_node('wscript')
+
+ Files are also searched from the directory 'base_path', to change it, set::
+
+ def dist(ctx):
+ ctx.base_path = path
+
+ :rtype: list of :py:class:`waflib.Node.Node`
+ """
+ try:
+ files = self.files
+ except AttributeError:
+ files = self.base_path.ant_glob('**/*', excl=self.get_excl())
+ return files
+
+def dist(ctx):
+ '''makes a tarball for redistributing the sources'''
+ pass
+
+class DistCheck(Dist):
+ """creates an archive with dist, then tries to build it"""
+ fun = 'distcheck'
+ cmd = 'distcheck'
+
+ def execute(self):
+ """
+ See :py:func:`waflib.Context.Context.execute`
+ """
+ self.recurse([os.path.dirname(Context.g_module.root_path)])
+ self.archive()
+ self.check()
+
+ def make_distcheck_cmd(self, tmpdir):
+ cfg = []
+ if Options.options.distcheck_args:
+ cfg = shlex.split(Options.options.distcheck_args)
+ else:
+ cfg = [x for x in sys.argv if x.startswith('-')]
+ cmd = [sys.executable, sys.argv[0], 'configure', 'build', 'install', 'uninstall', '--destdir=' + tmpdir] + cfg
+ return cmd
+
+ def check(self):
+ """
+ Creates the archive, uncompresses it and tries to build the project
+ """
+ import tempfile, tarfile
+
+ with tarfile.open(self.get_arch_name()) as t:
+ for x in t:
+ t.extract(x)
+
+ instdir = tempfile.mkdtemp('.inst', self.get_base_name())
+ cmd = self.make_distcheck_cmd(instdir)
+ ret = Utils.subprocess.Popen(cmd, cwd=self.get_base_name()).wait()
+ if ret:
+ raise Errors.WafError('distcheck failed with code %r' % ret)
+
+ if os.path.exists(instdir):
+ raise Errors.WafError('distcheck succeeded, but files were left in %s' % instdir)
+
+ shutil.rmtree(self.get_base_name())
+
+
+def distcheck(ctx):
+ '''checks if the project compiles (tarball from 'dist')'''
+ pass
+
+def autoconfigure(execute_method):
+ """
+ Decorator that enables context commands to run *configure* as needed.
+ """
+ def execute(self):
+ """
+ Wraps :py:func:`waflib.Context.Context.execute` on the context class
+ """
+ if not Configure.autoconfig:
+ return execute_method(self)
+
+ env = ConfigSet.ConfigSet()
+ do_config = False
+ try:
+ env.load(os.path.join(Context.top_dir, Options.lockfile))
+ except EnvironmentError:
+ Logs.warn('Configuring the project')
+ do_config = True
+ else:
+ if env.run_dir != Context.run_dir:
+ do_config = True
+ else:
+ h = 0
+ for f in env.files:
+ try:
+ h = Utils.h_list((h, Utils.readf(f, 'rb')))
+ except EnvironmentError:
+ do_config = True
+ break
+ else:
+ do_config = h != env.hash
+
+ if do_config:
+ cmd = env.config_cmd or 'configure'
+ if Configure.autoconfig == 'clobber':
+ tmp = Options.options.__dict__
+ if env.options:
+ Options.options.__dict__ = env.options
+ try:
+ run_command(cmd)
+ finally:
+ Options.options.__dict__ = tmp
+ else:
+ run_command(cmd)
+ run_command(self.cmd)
+ else:
+ return execute_method(self)
+ return execute
+Build.BuildContext.execute = autoconfigure(Build.BuildContext.execute)
+