Skip to content
Switch branches/tags
Go to file
Cannot retrieve contributors at this time
executable file 430 lines (338 sloc) 13.2 KB
#!/usr/bin/env python3
# Copyright 2013-2021 the openage authors. See for legal info.
openage autocancer-like cmake frontend.
Together with the Makefile, ./configure provides an autotools-like build
experience. For more info, see --help and doc/buildsystem.
import argparse
import os
import shlex
import shutil
import subprocess
import sys
if sys.version_info < (3, 6):
print("openage requires Python 3.6 or higher")
# argparsing
DESCRIPTION = """./configure is a convenience script:
it creates the build directory, symlinks it,
and invokes cmake for an out-of-source build.
Nobody is stopping you from skipping ./configure and our Makefile,
and using CMake directly (e.g. when packaging, or using an IDE).
For your convenience, ./configure even prints the direct CMake invocation!"""
EPILOG = """environment variables like CXX, CXXFLAGS, LDFLAGS are honored, \
but overwritten by command-line arguments."""
def getenv(*varnames, default=""):
fetches an environment variable.
tries all given varnames until it finds an existing one.
if none fits, returns default.
for var in varnames:
if var in os.environ:
return os.environ[var]
return default
def getenv_bool(varname):
fetches a "boolean" environment variable.
value = os.environ.get(varname)
if isinstance(value, str):
if value.lower() in {"0", "false", "no", "off", "n"}:
value = False
return bool(value)
# available optional features
# this defines the default activation for those:
# if_available: enable if it was found
# True: enable feature
# False: disable feature
# This 3-state activation allows distros to control the features definitively
# but independent compilations may still have autodetection.
"backtrace": "if_available",
"inotify": "if_available",
"opengl": "if_available",
"vulkan": "if_available",
"gperftools-tcmalloc": False,
"gperftools-profiler": "if_available",
"ncurses": "if_available",
def features(args, parser):
Enable or disable optional features.
If a feature is not explicitly enabled/disabled,
the defaults below will be used.
def sanitize_option_name(option):
""" Check if the given feature exists """
if option not in OPTIONS:
parser.error("unknown feature: '{}'.\n"
"available features:\n {}".format(
option, '\n '.join(OPTIONS)))
options = OPTIONS.copy()
if args.with_:
for arg in args.with_:
options[arg] = True
if args.without:
for arg in args.without:
options[arg] = False
return options
def build_type(args):
""" Set the cmake build type """
mode = args.mode
if mode == 'debug':
ret = 'Debug'
elif mode == 'release':
ret = 'Release'
return {
"build_type": ret
def get_compiler(args, parser):
Compute the compiler executable name
# determine compiler binaries from args.compiler
if args.compiler:
# map alias -> actual compiler
aliases = {
"clang": "clang++",
"gcc": "g++",
cxxver = args.compiler.split('-', maxsplit=1)
cxx = cxxver[0]
# try to replace aliases
if cxx in aliases:
cxx = aliases[cxx]
# we had a version suffix with e.g. -1.2.3
if len(cxxver) == 2:
cxx += "-" + cxxver[1]
# CXX has not been specified
if sys.platform.startswith('darwin'):
cxx = 'clang++'
# default to gnu compiler suite
cxx = 'g++'
# test whether the specified compiler actually exists
if not shutil.which(cxx):
parser.error('could not find c++ compiler executable: %s' % cxx)
return {
"cxx_compiler": cxx,
"cxx_flags": args.flags,
"exe_linker_flags": args.ldflags,
"module_linker_flags": args.ldflags,
"shared_linker_flags": args.ldflags,
def get_install_prefixes(args):
Determine the install prefix configuration.
ret = {
"install_prefix": args.prefix,
if args.py_prefix is not None:
ret["py_install_prefix"] = args.py_prefix
return ret
def bindir_creation(args, defines):
configuration for the sanitizer addons for gcc and clang.
def sanitize_for_filename(txt, fallback='-'):
sanitizes a string for safe usage in a filename
def yieldsanitizedchars():
""" generator for sanitizing the output folder name """
# False if the previous char was regular.
fallingback = True
for char in txt:
if char == fallback and fallingback:
fallingback = False
elif char.isalnum() or char in "+-_,":
fallingback = False
yield char
elif not fallingback:
fallingback = True
yield fallback
return "".join(yieldsanitizedchars())
bindir = ".bin/%s-%s-%s" % (
sanitize_for_filename("-O%s -sanitize=%s" % (
args.optimize, args.sanitize)))
if not args.dry_run:
os.makedirs(bindir, exist_ok=True)
def forcesymlink(linkto, name):
""" similar in function to ln -sf """
if args.dry_run:
except FileNotFoundError:
os.symlink(linkto, name)
# create the build dir and symlink it to 'bin'
forcesymlink(bindir, 'bin')
return bindir
def invoke_cmake(args, bindir, defines, options):
run cmake.
# the project root directory contains this configure file.
project_root = os.path.dirname(os.path.realpath(__file__))
# calculate cmake invocation from defines dict
invocation = [args.cmake_binary]
maxkeylen = max(len(k) for k in defines)
for key, val in sorted(defines.items()):
print('%s | %s' % (key.rjust(maxkeylen), val))
if key in {'cxx_compiler',}:
# work around this cmake 'feature':
# when run in an existing build directory, if CXX is given,
# all other arguments are ignored... this is retarded.
if os.path.exists(os.path.join(bindir, 'CMakeCache.txt')):
invocation.append('-DCMAKE_%s=%s' % (key.upper(), shlex.quote(val)))
invocation.extend(['-G', 'Ninja'])
if args.ccache:
if args.clang_tidy:
if args.download_nyan:
cxx_options = dict()
cxx_options["CXX_OPTIMIZATION_LEVEL"] = args.optimize
cxx_options["CXX_SANITIZE_MODE"] = args.sanitize
cxx_options["CXX_SANITIZE_FATAL"] = args.sanitize_fatal
for key, val in sorted(cxx_options.items()):
invocation.append('-D%s=%s' % (key, val))
print("\nconfig options:\n")
maxkeylen = max(len(k) for k in options)
for key, val in sorted(options.items()):
print('%s | %s' % (key.rjust(maxkeylen), val))
invocation.append('-DWANT_%s=%s' % (
key.upper().replace('-', '_'), val))
for raw_cmake_arg in args.raw_cmake_args:
if raw_cmake_arg == "--":
# look for traces of an in-source build
if os.path.isfile('CMakeCache.txt'):
print("\nwarning: found traces of an in-source build.")
print("CMakeCache.txt was deleted to make building possible.")
print("run 'make cleaninsourcebuild' to fully wipe the traces.")
# switch to build directory
print('\nbindir:\n%s/\n' % os.path.join(project_root, bindir))
if not args.dry_run:
# invoke cmake
print('invocation:\n%s\n' % ' '.join(invocation))
if args.dry_run:
print("(now running cmake:)\n")
except FileNotFoundError:
print("cmake was not found")
def main(args, parser):
Compose the cmake invocation.
Basically does what many distro package managers do as well.
try:['cowsay', '--', DESCRIPTION])
except (FileNotFoundError, PermissionError):
defines = {}
options = features(args, parser)
defines.update(get_compiler(args, parser))
bindir = bindir_creation(args, defines)
invoke_cmake(args, bindir, defines, options)
def parse_args():
""" argument parsing """
cli = argparse.ArgumentParser(
cli.add_argument("--mode", "-m",
choices=["debug", "release"],
default=getenv("BUILDMODE", default="debug"),
help="controls cmake build mode")
cli.add_argument("--optimize", "-O",
choices=["auto", "0", "1", "g", "2", "3", "max"],
default=getenv("OPTIMIZE", default="auto"),
help=("controls optimization-related flags. " +
"is set according to mode if 'auto'. " +
"conflicts with --flags"))
choices=["none", "yes", "mem", "thread"],
default=getenv("SANITIZER", default="none"),
help=("enable one of those (run-time) code sanitizers."
"'yes' enables the address and "
"undefined sanitizers."))
cli.add_argument("--sanitize-fatal", action='store_true',
help="With --sanitize, stop execution on first problem.")
cli.add_argument("--compiler", "-c",
help="c++ compiler executable, default=$ENV[CXX]")
cli.add_argument("--with", action='append', dest='with_', metavar='OPTION',
help="enable optional functionality. "
"for a list of available features, "
"use --list-options")
cli.add_argument("--without", action='append', metavar='OPTION',
help="disable optional functionality. "
"for a list of available features, "
"use --list-options")
cli.add_argument("--list-options", action="store_true",
help="list available optional feature switches")
cli.add_argument("--flags", "-f",
default=getenv("CXXFLAGS", "CCFLAGS", "CFLAGS"),
help="compiler flags")
cli.add_argument("--ldflags", "-l",
help="linker flags")
cli.add_argument("--prefix", "-p", default="/usr/local",
help="installation directory prefix")
cli.add_argument("--py-prefix", default=None,
help="python module installation directory prefix")
cli.add_argument("--dry-run", action='store_true',
help="just print the cmake invocation without calling it")
cli.add_argument("--cmake-binary", default="cmake",
help="path to the cmake binary")
cli.add_argument("--ninja", action="store_true",
help="use ninja instead of GNU make")
cli.add_argument("--ccache", action="store_true",
help="activate using the ccache compiler cache")
cli.add_argument("--clang-tidy", action="store_true",
help="emit clang-tidy analysis messages")
cli.add_argument("--download-nyan", action="store_true",
help="enable automatic download of the nyan project")
# arguments after -- are used as raw cmake args
cli.add_argument('raw_cmake_args', nargs=argparse.REMAINDER, default=[],
help="all args after ' -- ' are passed directly to cmake")
args = cli.parse_args()
if args.sanitize == 'none' and args.sanitize_fatal:
cli.error('--sanitize-fatal only valid with --sanitize')
if args.list_options:
header = "{} | Default state".format("Optional features:".ljust(25))
print("{}\n{}".format(header, "-" * len(header)))
for option, state in sorted(OPTIONS.items()):
state_str = (state if not isinstance(state, bool)
else ("on" if state else "off"))
print("{} | {}".format(option.ljust(25), state_str))
return args, cli
if __name__ == "__main__":