aboutsummaryrefslogtreecommitdiffstats
path: root/build_scripts/qp5_tool.py
diff options
context:
space:
mode:
Diffstat (limited to 'build_scripts/qp5_tool.py')
-rw-r--r--build_scripts/qp5_tool.py296
1 files changed, 235 insertions, 61 deletions
diff --git a/build_scripts/qp5_tool.py b/build_scripts/qp5_tool.py
index aca8cf7f6..0dea03b27 100644
--- a/build_scripts/qp5_tool.py
+++ b/build_scripts/qp5_tool.py
@@ -40,29 +40,39 @@
from __future__ import print_function
from argparse import ArgumentParser, RawTextHelpFormatter
+import datetime
+from enum import Enum
import os
import re
import subprocess
import sys
+import time
import warnings
-desc = """
+
+DESC = """
Utility script for working with Qt for Python.
Feel free to extend!
-qp5_tool.py can be configured by creating a configuration file
-in the format key=value:
- "%CONFIGFILE%"
+Typical Usage:
+Update and build a repository: python qp5_tool -p -b
+
+qp5_tool.py uses a configuration file "%CONFIGFILE%"
+in the format key=value.
-It is possible to use repository-specific values
-by adding a key postfixed by a dash and the repository folder base name, eg:
+It is possible to use repository-specific values by adding a key postfixed by
+a dash and the repository folder base name, eg:
Modules-pyside-setup512=Core,Gui,Widgets,Network,Test
Configuration keys:
+Acceleration Incredibuild or unset
+BuildArguments Arguments to setup.py
+Generator Generator to be used for CMake. Currently, only Ninja is
+ supported.
+Jobs Number of jobs to be run simultaneously
Modules Comma separated list of modules to be built
(for --module-subset=)
-BuildArguments Arguments to setup.py
Python Python executable (Use python_d for debug builds on Windows)
Arbitrary keys can be defined and referenced by $(name):
@@ -72,10 +82,41 @@ Modules=$(MinimalModules),Multimedia
Modules-pyside-setup-minimal=$(MinimalModules)
"""
+
+class Acceleration(Enum):
+ NONE = 0
+ INCREDIBUILD = 1
+
+
+class BuildMode(Enum):
+ NONE = 0
+ BUILD = 1
+ RECONFIGURE = 2
+ MAKE = 3
+
+
+DEFAULT_BUILD_ARGS = ['--build-tests', '--skip-docs', '--quiet']
+IS_WINDOWS = sys.platform == 'win32'
+INCREDIBUILD_CONSOLE = 'BuildConsole' if IS_WINDOWS else '/opt/incredibuild/bin/ib_console'
+# Config file keys
+ACCELERATION_KEY = 'Acceleration'
+BUILDARGUMENTS_KEY = 'BuildArguments'
+GENERATOR_KEY = 'Generator'
+JOBS_KEY = 'Jobs'
+MODULES_KEY = 'Modules'
+PYTHON_KEY = 'Python'
+
+DEFAULT_MODULES = "Core,Gui,Widgets,Network,Test,Qml,Quick,Multimedia,MultimediaWidgets"
+DEFAULT_CONFIG_FILE = "Modules={}\n".format(DEFAULT_MODULES)
+
+build_mode = BuildMode.NONE
+opt_dry_run = False
+
+
def which(needle):
"""Perform a path search"""
needles = [needle]
- if is_windows:
+ if IS_WINDOWS:
for ext in ("exe", "bat", "cmd"):
needles.append("{}.{}".format(needle, ext))
@@ -86,47 +127,94 @@ def which(needle):
return binary
return None
+
+def command_log_string(args, dir):
+ result = '[{}]'.format(os.path.basename(dir))
+ for arg in args:
+ result += ' "{}"'.format(arg) if ' ' in arg else ' {}'.format(arg)
+ return result
+
+
def execute(args):
"""Execute a command and print to log"""
- log_string = '[{}] {}'.format(os.path.basename(os.getcwd()), ' '.join(args))
+ log_string = command_log_string(args, os.getcwd())
print(log_string)
+ if opt_dry_run:
+ return
exit_code = subprocess.call(args)
if exit_code != 0:
raise RuntimeError('FAIL({}): {}'.format(exit_code, log_string))
+
+def run_process_output(args):
+ """Run a process and return its output. Also run in dry_run mode"""
+ std_out = subprocess.Popen(args, universal_newlines=1,
+ stdout=subprocess.PIPE).stdout
+ result = [line.rstrip() for line in std_out.readlines()]
+ std_out.close()
+ return result
+
+
def run_git(args):
"""Run git in the current directory and its submodules"""
- args.insert(0, git) # run in repo
+ args.insert(0, git) # run in repo
execute(args) # run for submodules
module_args = [git, "submodule", "foreach"]
module_args.extend(args)
execute(module_args)
-def expand_reference(dict, value):
+
+def expand_reference(cache_dict, value):
"""Expand references to other keys in config files $(name) by value."""
pattern = re.compile(r"\$\([^)]+\)")
while True:
- match = pattern.match(value)
- if not match:
- break
- key = match.group(0)[2:-1]
- value = value[:match.start(0)] + dict[key] + value[match.end(0):]
+ match = pattern.match(value)
+ if not match:
+ break
+ key = match.group(0)[2:-1]
+ value = value[:match.start(0)] + cache_dict[key] + value[match.end(0):]
return value
+
+def editor():
+ editor = os.getenv('EDITOR')
+ if not editor:
+ return 'notepad' if IS_WINDOWS else 'vi'
+ editor = editor.strip()
+ if IS_WINDOWS:
+ # Windows: git requires quotes in the variable
+ if editor.startswith('"') and editor.endswith('"'):
+ editor = editor[1:-1]
+ editor = editor.replace('/', '\\')
+ return editor
+
+
+def edit_config_file():
+ exit_code = -1
+ try:
+ exit_code = subprocess.call([editor(), config_file])
+ except Exception as e:
+ reason = str(e)
+ print('Unable to launch: {}: {}'.format(editor(), reason))
+ return exit_code
+
+
"""
Config file handling, cache and read function
"""
-
config_dict = {}
-def read_config_file(fileName):
+
+def read_config_file(file_name):
+ """Read the config file into config_dict, expanding continuation lines"""
global config_dict
keyPattern = re.compile(r'^\s*([A-Za-z0-9\_\-]+)\s*=\s*(.*)$')
- with open(config_file) as f:
+ with open(file_name) as f:
while True:
- line = f.readline().rstrip()
+ line = f.readline()
if not line:
break
+ line = line.rstrip()
match = keyPattern.match(line)
if match:
key = match.group(1)
@@ -136,7 +224,8 @@ def read_config_file(fileName):
value += f.readline().rstrip()
config_dict[key] = expand_reference(config_dict, value)
-def read_tool_config(key):
+
+def read_config(key):
"""
Read a value from the '$HOME/.qp5_tool' configuration file. When given
a key 'key' for the repository directory '/foo/qt-5', check for the
@@ -147,43 +236,122 @@ def read_tool_config(key):
repo_value = config_dict.get(key + '-' + base_dir)
return repo_value if repo_value else config_dict.get(key)
+
+def read_bool_config(key):
+ value = read_config(key)
+ return value and value in ['1', 'true', 'True']
+
+
+def read_int_config(key, default=-1):
+ value = read_config(key)
+ return int(value) if value else default
+
+
+def read_acceleration_config():
+ value = read_config(ACCELERATION_KEY)
+ if value:
+ value = value.lower()
+ if value == 'incredibuild':
+ return Acceleration.INCREDIBUILD
+ return Acceleration.NONE
+
+
def read_config_build_arguments():
- value = read_tool_config('BuildArguments')
+ value = read_config(BUILDARGUMENTS_KEY)
if value:
return re.split(r'\s+', value)
- return default_build_args;
+ return DEFAULT_BUILD_ARGS
+
def read_config_modules_argument():
- value = read_tool_config('Modules')
+ value = read_config(MODULES_KEY)
if value and value != '' and value != 'all':
return '--module-subset=' + value
return None
+
def read_config_python_binary():
- binary = read_tool_config('Python')
- return binary if binary else 'python'
+ binary = read_config(PYTHON_KEY)
+ if binary:
+ return binary
+ # Use 'python3' unless virtualenv is set
+ use_py3 = (not os.environ.get('VIRTUAL_ENV') and which('python3'))
+ return 'python3' if use_py3 else 'python'
+
-def get_config_file():
+def get_config_file(base_name):
home = os.getenv('HOME')
- if is_windows:
+ if IS_WINDOWS:
# Set a HOME variable on Windows such that scp. etc.
# feel at home (locating .ssh).
if not home:
home = os.getenv('HOMEDRIVE') + os.getenv('HOMEPATH')
os.environ['HOME'] = home
user = os.getenv('USERNAME')
- config_file = os.path.join(os.getenv('APPDATA'), config_file_name)
+ config_file = os.path.join(os.getenv('APPDATA'), base_name)
else:
user = os.getenv('USER')
config_dir = os.path.join(home, '.config')
if os.path.exists(config_dir):
- config_file = os.path.join(config_dir, config_file_name)
+ config_file = os.path.join(config_dir, base_name)
else:
- config_file = os.path.join(home, '.' + config_file_name)
+ config_file = os.path.join(home, '.' + base_name)
return config_file
-def get_options(desc):
+
+def build(target):
+ """Run configure and build steps"""
+ start_time = time.time()
+
+ arguments = []
+ acceleration = read_acceleration_config()
+ if not IS_WINDOWS and acceleration == Acceleration.INCREDIBUILD:
+ arguments.append(INCREDIBUILD_CONSOLE)
+ arguments.append('--avoid') # caching, v0.96.74
+ arguments.extend([read_config_python_binary(), 'setup.py', target])
+ arguments.extend(read_config_build_arguments())
+ generator = read_config(GENERATOR_KEY)
+ if generator == 'Ninja':
+ arguments.extend(['--make-spec', 'ninja'])
+ jobs = read_int_config(JOBS_KEY)
+ if jobs > 1:
+ arguments.extend(['-j', str(jobs)])
+ if build_mode != BuildMode.BUILD:
+ arguments.extend(['--reuse-build', '--ignore-git'])
+ if build_mode != BuildMode.RECONFIGURE:
+ arguments.append('--skip-cmake')
+ modules = read_config_modules_argument()
+ if modules:
+ arguments.append(modules)
+ if IS_WINDOWS and acceleration == Acceleration.INCREDIBUILD:
+ arg_string = ' '.join(arguments)
+ arguments = [INCREDIBUILD_CONSOLE, '/command={}'.format(arg_string)]
+
+ execute(arguments)
+
+ elapsed_time = int(time.time() - start_time)
+ print('--- Done({}s) ---'.format(elapsed_time))
+
+
+def run_tests():
+ """Run tests redirected into a log file with a time stamp"""
+ logfile_name = datetime.datetime.today().strftime("test_%Y%m%d_%H%M.txt")
+ binary = sys.executable
+ command = '"{}" testrunner.py test > {}'.format(binary, logfile_name)
+ print(command_log_string([command], os.getcwd()))
+ start_time = time.time()
+ result = 0 if opt_dry_run else os.system(command)
+ elapsed_time = int(time.time() - start_time)
+ print('--- Done({}s) ---'.format(elapsed_time))
+ return result
+
+
+def create_argument_parser(desc):
parser = ArgumentParser(description=desc, formatter_class=RawTextHelpFormatter)
+ parser.add_argument('--dry-run', '-d', action='store_true',
+ help='Dry run, print commands')
+ parser.add_argument('--edit', '-e', action='store_true',
+ help='Edit config file')
parser.add_argument('--reset', '-r', action='store_true',
help='Git reset hard to upstream state')
parser.add_argument('--clean', '-c', action='store_true',
@@ -193,45 +361,55 @@ def get_options(desc):
parser.add_argument('--build', '-b', action='store_true',
help='Build (configure + build)')
parser.add_argument('--make', '-m', action='store_true', help='Make')
+ parser.add_argument('--no-install', '-n', action='store_true',
+ help='Run --build only, do not install')
parser.add_argument('--Make', '-M', action='store_true',
help='cmake + Make (continue broken build)')
+ parser.add_argument('--test', '-t', action='store_true',
+ help='Run tests')
parser.add_argument('--version', '-v', action='version', version='%(prog)s 1.0')
+ return parser
- return parser.parse_args()
if __name__ == '__main__':
-
git = None
base_dir = None
- default_build_args = ['--build-tests', '--skip-docs', '--quiet']
- is_windows = sys.platform == 'win32'
- config_file_name = 'qp5_tool.conf'
config_file = None
user = None
- default_config_file = """
- Modules=Core,Gui,Widgets,Network,Test,Qml,Quick,Multimedia,MultimediaWidgets
- BuildArguments={}
- # Python executable (python_d for debug builds)
- Python=python
- """
- config_file = get_config_file()
- desc = desc.replace('%CONFIGFILE%', config_file)
- options = get_options(desc)
+ config_file = get_config_file('qp5_tool.conf')
+ argument_parser = create_argument_parser(DESC.replace('%CONFIGFILE%', config_file))
+ options = argument_parser.parse_args()
+ opt_dry_run = options.dry_run
- git = which('git')
- if git is None:
+ if options.edit:
+ sys.exit(edit_config_file())
+
+ if options.build:
+ build_mode = BuildMode.BUILD
+ elif options.make:
+ build_mode = BuildMode.MAKE
+ elif options.Make:
+ build_mode = BuildMode.RECONFIGURE
+
+ if build_mode == BuildMode.NONE and not (options.clean or options.reset
+ or options.pull or options.test):
+ argument_parser.print_help()
+ sys.exit(0)
+
+ git = 'git'
+ if which(git) is None:
warnings.warn('Unable to find git', RuntimeWarning)
sys.exit(-1)
if not os.path.exists(config_file):
print('Create initial config file ', config_file, " ..")
with open(config_file, 'w') as f:
- f.write(default_config_file.format(' '.join(default_build_args)))
+ f.write(DEFAULT_CONFIG_FILE.format(' '.join(DEFAULT_BUILD_ARGS)))
while not os.path.exists('.gitmodules'):
cwd = os.getcwd()
- if cwd == '/' or (is_windows and len(cwd) < 4):
+ if cwd == '/' or (IS_WINDOWS and len(cwd) < 4):
warnings.warn('Unable to find git root', RuntimeWarning)
sys.exit(-1)
os.chdir(os.path.dirname(cwd))
@@ -247,15 +425,11 @@ if __name__ == '__main__':
if options.pull:
run_git(['pull', '--rebase'])
- if options.build or options.make or options.Make:
- arguments = [read_config_python_binary(), 'setup.py', 'install']
- arguments.extend(read_config_build_arguments())
- if options.make or options.Make:
- arguments.extend(['--reuse-build', '--ignore-git'])
- if not options.Make:
- arguments.append('--skip-cmake')
- modules = read_config_modules_argument()
- if modules:
- arguments.append(modules)
- execute(arguments)
+ if build_mode != BuildMode.NONE:
+ target = 'build' if options.no_install else 'install'
+ build(target)
+
+ if options.test:
+ sys.exit(run_tests())
+
sys.exit(0)