mirror of
https://gitlab.com/ita1024/waf.git
synced 2024-11-22 18:07:12 +01:00
614 lines
16 KiB
Python
614 lines
16 KiB
Python
#!/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
|
|
|
|
if not Context.run_dir:
|
|
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(os.path.normpath(os.path.join(Context.run_dir, Context.WSCRIPT_FILE)))
|
|
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)
|
|
|