aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/project/build.py
diff options
context:
space:
mode:
Diffstat (limited to 'project/build.py')
-rw-r--r--project/build.py265
1 files changed, 265 insertions, 0 deletions
diff --git a/project/build.py b/project/build.py
new file mode 100644
index 0000000..ff366c3
--- /dev/null
+++ b/project/build.py
@@ -0,0 +1,265 @@
+# Copyright (c) 2019 Egor Tensin <Egor.Tensin@gmail.com>
+# This file is part of the "cmake-common" project.
+# For details, see https://github.com/egor-tensin/cmake-common.
+# Distributed under the MIT License.
+
+R'''Build a CMake project.
+
+This script is used basically to invoke the CMake executable in a
+cross-platform way (provided the platform has Python 3, of course). The
+motivation was to merge my Travis and AppVeyor build scripts (largely similar,
+but written in bash and PowerShell, respectively).
+
+A simple usage example:
+
+ $ project-build --configuration Release --install path/to/somewhere -- examples/simple build/
+ ...
+
+ $ ./path/to/somewhere/bin/foo
+ foo
+'''
+
+import argparse
+from contextlib import contextmanager
+import logging
+import os
+import sys
+import tempfile
+
+from project.configuration import Configuration
+from project.platform import Platform
+from project.toolset import Toolset, ToolsetVersion
+from project.utils import normalize_path, mkdir_parent, run, setup_logging
+import project.version
+
+
+DEFAULT_PLATFORM = Platform.AUTO
+DEFAULT_CONFIGURATION = Configuration.DEBUG
+DEFAULT_TOOLSET_VERSION = ToolsetVersion.default()
+
+
+# This way of basically passing `-j` to make is more universal compared to
+# _guessing_ that the build system is make and passing -j explicitly. Plus it
+# works with older CMake versions, which don't support the --parallel flag.
+cmake_env = os.environ.copy()
+cmake_env['CMAKE_BUILD_PARALLEL_LEVEL'] = str(os.cpu_count())
+
+
+def run_cmake(cmake_args):
+ return run(['cmake'] + cmake_args, env=cmake_env)
+
+
+class GenerationPhase:
+ def __init__(self, src_dir, build_dir, install_dir=None, platform=None,
+ configuration=None, boost_dir=None, cmake_args=None):
+ src_dir = normalize_path(src_dir)
+ build_dir = normalize_path(build_dir)
+ if install_dir is not None:
+ install_dir = normalize_path(install_dir)
+ platform = platform or DEFAULT_PLATFORM
+ configuration = configuration or DEFAULT_CONFIGURATION
+ if boost_dir is not None:
+ boost_dir = normalize_path(boost_dir)
+ cmake_args = cmake_args or []
+
+ self.src_dir = src_dir
+ self.build_dir = build_dir
+ self.install_dir = install_dir
+ self.platform = platform
+ self.configuration = configuration
+ self.boost_dir = boost_dir
+ self.cmake_args = cmake_args
+
+ def _cmake_args(self, toolset):
+ result = []
+ result += toolset.cmake_args(self.build_dir, self.platform)
+ result += self.configuration.cmake_args()
+ result += self._cmake_boost_args()
+ result += self._cmake_extra_args()
+ result += self.cmake_args
+ # Important! -H must come as the last parameter, older CMake versions
+ # don't like it when it's not.
+ result += self._cmake_dir_args()
+ return result
+
+ def _cmake_boost_args(self):
+ if self.boost_dir is None:
+ return []
+ root = self.boost_dir
+ librarydir = self.platform.boost_librarydir(self.configuration)
+ librarydir = os.path.join(self.boost_dir, librarydir)
+ return [
+ f'-DBOOST_ROOT={root}',
+ f'-DBOOST_LIBRARYDIR={librarydir}',
+ ]
+
+ @staticmethod
+ def _cmake_extra_args():
+ return ['-DCMAKE_EXPORT_COMPILE_COMMANDS=ON']
+
+ def _cmake_dir_args(self):
+ args = []
+ if self.install_dir is not None:
+ args += [f'-DCMAKE_INSTALL_PREFIX={self.install_dir}']
+ # Important! -H must come as the last parameter, older CMake versions
+ # don't like it when it's not.
+ args += [
+ f'-B{self.build_dir}',
+ f'-H{self.src_dir}'
+ ]
+ return args
+
+ def run(self, toolset):
+ run_cmake(self._cmake_args(toolset))
+
+
+class BuildPhase:
+ def __init__(self, build_dir, install_dir=None, configuration=None):
+
+ build_dir = normalize_path(build_dir)
+ configuration = configuration or DEFAULT_CONFIGURATION
+
+ self.build_dir = build_dir
+ self.install_dir = install_dir
+ self.configuration = configuration
+
+ def _cmake_args(self, toolset):
+ result = ['--build', self.build_dir]
+ result += ['--config', str(self.configuration)]
+ if self.install_dir is not None:
+ result += ['--target', 'install']
+ result += ['--'] + toolset.build_system_args()
+ return result
+
+ def run(self, toolset):
+ run_cmake(self._cmake_args(toolset))
+
+
+class BuildParameters:
+ BUILD_DIR_TMP_PLACEHOLDER = 'TMP'
+
+ def __init__(self, src_dir, build_dir, install_dir=None,
+ platform=None, configuration=None, boost_dir=None,
+ toolset_version=None, cmake_args=None):
+
+ src_dir = normalize_path(src_dir)
+ build_dir = self.normalize_build_dir(build_dir)
+ if install_dir is not None:
+ install_dir = normalize_path(install_dir)
+ platform = platform or DEFAULT_PLATFORM
+ configuration = configuration or DEFAULT_CONFIGURATION
+ if boost_dir is not None:
+ boost_dir = normalize_path(boost_dir)
+ toolset_version = toolset_version or DEFAULT_TOOLSET_VERSION
+ cmake_args = cmake_args or []
+
+ self.src_dir = src_dir
+ self.build_dir = build_dir
+ self.install_dir = install_dir
+ self.platform = platform
+ self.configuration = configuration
+ self.boost_dir = boost_dir
+ self.toolset_version = toolset_version
+ self.cmake_args = cmake_args
+
+ @staticmethod
+ def from_args(args):
+ args = vars(args)
+ args.pop('help_toolsets', None)
+ return BuildParameters(**args)
+
+ @staticmethod
+ def normalize_build_dir(build_dir):
+ if build_dir == BuildParameters.BUILD_DIR_TMP_PLACEHOLDER:
+ return build_dir
+ return normalize_path(build_dir)
+
+ @contextmanager
+ def create_build_dir(self):
+ if self.build_dir != BuildParameters.BUILD_DIR_TMP_PLACEHOLDER:
+ logging.info('Build directory: %s', self.build_dir)
+ mkdir_parent(self.build_dir)
+ yield self.build_dir
+ return
+
+ with tempfile.TemporaryDirectory(dir=os.path.dirname(self.src_dir)) as build_dir:
+ logging.info('Build directory: %s', build_dir)
+ try:
+ yield build_dir
+ finally:
+ logging.info('Removing build directory: %s', build_dir)
+ return
+
+
+def build(params):
+ with params.create_build_dir() as build_dir:
+ toolset = Toolset.make(params.toolset_version, params.platform)
+
+ gen_phase = GenerationPhase(params.src_dir, build_dir,
+ install_dir=params.install_dir,
+ platform=params.platform,
+ configuration=params.configuration,
+ boost_dir=params.boost_dir,
+ cmake_args=params.cmake_args)
+ gen_phase.run(toolset)
+ build_phase = BuildPhase(build_dir, install_dir=params.install_dir,
+ configuration=params.configuration)
+ build_phase.run(toolset)
+
+
+def _parse_args(argv=None):
+ if argv is None:
+ argv = sys.argv[1:]
+
+ if '--help-toolsets' in argv:
+ sys.stdout.write(ToolsetVersion.help_toolsets())
+ sys.exit(0)
+
+ parser = argparse.ArgumentParser(
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter)
+
+ project.version.add_to_arg_parser(parser)
+
+ parser.add_argument('--install', metavar='DIR', dest='install_dir',
+ type=normalize_path,
+ help='install directory')
+
+ platform_options = '/'.join(map(str, Platform.all()))
+ configuration_options = '/'.join(map(str, Configuration.all()))
+
+ parser.add_argument('--platform', metavar='PLATFORM',
+ type=Platform.parse,
+ help=f'target platform ({platform_options})')
+ parser.add_argument('--configuration', metavar='CONFIG',
+ type=Configuration.parse, default=DEFAULT_CONFIGURATION,
+ help=f'build configuration ({configuration_options})')
+
+ parser.add_argument('--boost', metavar='DIR', dest='boost_dir',
+ type=normalize_path,
+ help='set Boost directory path')
+
+ parser.add_argument('--toolset', metavar='TOOLSET', dest='toolset_version',
+ type=ToolsetVersion.parse, default=DEFAULT_TOOLSET_VERSION,
+ help=f'toolset to use ({ToolsetVersion.usage()})')
+ parser.add_argument('--help-toolsets', action='store_true',
+ help='show detailed info about supported toolsets')
+
+ parser.add_argument('src_dir', type=normalize_path,
+ help='source directory')
+ parser.add_argument('build_dir', type=BuildParameters.normalize_build_dir,
+ help=f"build directory ('{BuildParameters.BUILD_DIR_TMP_PLACEHOLDER}' to use a temporary directory)")
+ parser.add_argument('cmake_args', nargs='*', default=[],
+ help='additional CMake arguments, to be passed verbatim')
+
+ return parser.parse_args(argv)
+
+
+def main(argv=None):
+ args = _parse_args(argv)
+ with setup_logging():
+ build(BuildParameters.from_args(args))
+
+
+if __name__ == '__main__':
+ main()