1#!/usr/local/bin/python3.8 2 3# Copyright 2013-2016 the openage authors. See copying.md for legal info. 4 5""" 6openage autocancer-like cmake frontend. 7 8Together with the Makefile, ./configure provides an autotools-like build 9experience. For more info, see --help and doc/buildsystem. 10""" 11 12import argparse 13import os 14import shlex 15import shutil 16import subprocess 17import sys 18 19if sys.version_info < (3, 4): 20 print("openage requires Python 3.4 or higher") 21 exit(1) 22 23 24# argparsing 25DESCRIPTION = """./configure is a convenience script: 26it creates the build directory, symlinks it, 27and invokes cmake for an out-of-source build. 28 29Nobody is stopping you from skipping ./configure and our Makefile, 30and using CMake directly (e.g. when packaging, or using an IDE). 31For your convenience, ./configure even prints the direct CMake invocation!""" 32 33EPILOG = """environment variables like CXX, CXXFLAGS, LDFLAGS are honored, \ 34but overwritten by command-line arguments.""" 35 36 37def getenv(*varnames, default=""): 38 """ 39 fetches an environment variable. 40 tries all given varnames until it finds an existing one. 41 if none fits, returns default. 42 """ 43 for var in varnames: 44 if var in os.environ: 45 return os.environ[var] 46 47 return default 48 49 50def getenv_bool(varname): 51 """ 52 fetches a "boolean" environment variable. 53 """ 54 value = os.environ.get(varname) 55 if isinstance(value, str): 56 if value.lower() in {"0", "false", "no", "off", "n"}: 57 value = False 58 59 return bool(value) 60 61 62# available optional features 63# this defines the default activation for those: 64# if_available: enable if it was found 65# True: enable feature 66# False: disable feature 67# This 3-state activation allows distros to control the features definitively 68# but independent compilations may still have autodetection. 69OPTIONS = { 70 "backtrace": "if_available", 71 "inotify": "if_available", 72 "gperftools-tcmalloc": False, 73 "gperftools-profiler": "if_available", 74} 75 76 77def features(args, parser): 78 """ 79 Enable or disable optional features. 80 If a feature is not explicitly enabled/disabled, 81 the defaults below will be used. 82 """ 83 84 85 def sanitize_option_name(option): 86 """ Check if the given feature exists """ 87 if option not in OPTIONS: 88 parser.error("unknown feature: '{}'.\n" 89 "available features:\n {}".format( 90 option, '\n '.join(OPTIONS))) 91 92 options = OPTIONS.copy() 93 94 if args.with_: 95 for arg in args.with_: 96 sanitize_option_name(arg) 97 options[arg] = True 98 99 if args.without: 100 for arg in args.without: 101 sanitize_option_name(arg) 102 options[arg] = False 103 104 return options 105 106 107def build_type(args): 108 """ Set the cmake build type """ 109 mode = args.mode 110 if mode == 'debug': 111 ret = 'Debug' 112 elif mode == 'release': 113 ret = 'Release' 114 115 return { 116 "build_type": ret 117 } 118 119 120def get_compiler(args, parser): 121 """ 122 Compute the compiler executable name 123 """ 124 125 # determine compiler binaries from args.compiler 126 if args.compiler: 127 # map alias -> actual compiler 128 aliases = { 129 "clang": "clang++", 130 "gcc": "g++", 131 } 132 133 cxx = args.compiler 134 135 # try to replace aliases 136 if cxx in aliases: 137 cxx = aliases[cxx] 138 139 else: 140 # CXX has not been specified 141 if sys.platform.startswith('darwin'): 142 cxx = 'clang++' 143 else: 144 # default to gnu compiler suite 145 cxx = 'g++' 146 147 # test whether the specified compiler actually exists 148 if not shutil.which(cxx): 149 parser.error('could not find c++ compiler executable: %s' % cxx) 150 151 return { 152 "cxx_compiler": cxx, 153 "cxx_flags": args.flags, 154 "exe_linker_flags": args.ldflags, 155 "module_linker_flags": args.ldflags, 156 "shared_linker_flags": args.ldflags, 157 } 158 159 160def get_install_prefixes(args): 161 """ 162 Determine the install prefix configuration. 163 """ 164 165 ret = { 166 "install_prefix": args.prefix, 167 } 168 169 if args.py_prefix is not None: 170 ret["py_install_prefix"] = args.py_prefix 171 172 return ret 173 174 175def bindir_creation(args, defines): 176 """ 177 configuration for the sanitizer addons for gcc and clang. 178 """ 179 180 def sanitize_for_filename(txt, fallback='-'): 181 """ 182 sanitizes a string for safe usage in a filename 183 """ 184 185 def yieldsanitizedchars(): 186 """ generator for sanitizing the output folder name """ 187 # False if the previous char was regular. 188 fallingback = True 189 for char in txt: 190 if char == fallback and fallingback: 191 fallingback = False 192 elif char.isalnum() or char in "+-_,": 193 fallingback = False 194 yield char 195 elif not fallingback: 196 fallingback = True 197 yield fallback 198 199 return "".join(yieldsanitizedchars()) 200 201 bindir = ".bin/%s-%s-%s" % ( 202 sanitize_for_filename(defines["cxx_compiler"]), 203 sanitize_for_filename(args.mode), 204 sanitize_for_filename("-O%s -sanitize=%s" % ( 205 args.optimize, args.sanitize))) 206 207 if not args.dry_run: 208 os.makedirs(bindir, exist_ok=True) 209 210 211 def forcesymlink(linkto, name): 212 """ similar in function to ln -sf """ 213 if args.dry_run: 214 return 215 216 try: 217 os.unlink(name) 218 except FileNotFoundError: 219 pass 220 221 os.symlink(linkto, name) 222 223 # create the build dir and symlink it to 'bin' 224 forcesymlink(bindir, 'bin') 225 226 return bindir 227 228 229def invoke_cmake(args, bindir, defines, options): 230 """ 231 run cmake. 232 """ 233 234 # the project root directory contains this configure file. 235 project_root = os.path.dirname(os.path.realpath(__file__)) 236 237 # calculate cmake invocation from defines dict 238 invocation = [args.cmake_binary] 239 maxkeylen = max(len(k) for k in defines) 240 for key, val in sorted(defines.items()): 241 print('%s | %s' % (key.rjust(maxkeylen), val)) 242 243 if key in {'cxx_compiler',}: 244 # work around this cmake 'feature': 245 # when run in an existing build directory, if CXX is given, 246 # all other arguments are ignored... this is retarded. 247 if os.path.exists(os.path.join(bindir, 'CMakeCache.txt')): 248 continue 249 250 invocation.append('-DCMAKE_%s=%s' % (key.upper(), shlex.quote(val))) 251 252 if args.ninja: 253 invocation.extend(['-G', 'Ninja']) 254 255 if args.ccache: 256 invocation.append('-DENABLE_CCACHE=ON') 257 258 if args.clang_tidy: 259 invocation.append('-DENABLE_CLANG_TIDY=ON') 260 261 if args.download_nyan: 262 invocation.append("-DDOWNLOAD_NYAN=YES") 263 264 cxx_options = dict() 265 cxx_options["CXX_OPTIMIZATION_LEVEL"] = args.optimize 266 cxx_options["CXX_SANITIZE_MODE"] = args.sanitize 267 cxx_options["CXX_SANITIZE_FATAL"] = args.sanitize_fatal 268 for key, val in sorted(cxx_options.items()): 269 invocation.append('-D%s=%s' % (key, val)) 270 271 print("\nconfig options:\n") 272 273 maxkeylen = max(len(k) for k in options) 274 for key, val in sorted(options.items()): 275 print('%s | %s' % (key.rjust(maxkeylen), val)) 276 277 invocation.append('-DWANT_%s=%s' % ( 278 key.upper().replace('-', '_'), val)) 279 280 for raw_cmake_arg in args.raw_cmake_args: 281 if raw_cmake_arg == "--": 282 continue 283 284 invocation.append(raw_cmake_arg) 285 286 invocation.append('--') 287 invocation.append(project_root) 288 289 # look for traces of an in-source build 290 291 if os.path.isfile('CMakeCache.txt'): 292 print("\nwarning: found traces of an in-source build.") 293 print("CMakeCache.txt was deleted to make building possible.") 294 print("run 'make cleaninsourcebuild' to fully wipe the traces.") 295 os.remove('CMakeCache.txt') 296 297 # switch to build directory 298 print('\nbindir:\n%s/\n' % os.path.join(project_root, bindir)) 299 if not args.dry_run: 300 os.chdir(bindir) 301 302 # invoke cmake 303 try: 304 print('invocation:\n%s\n' % ' '.join(invocation)) 305 if args.dry_run: 306 exit(0) 307 else: 308 print("(now running cmake:)\n") 309 exit(subprocess.call(invocation)) 310 except FileNotFoundError: 311 print("cmake was not found") 312 exit(1) 313 314 315def main(args, parser): 316 """ 317 Compose the cmake invocation. 318 Basically does what many distro package managers do as well. 319 """ 320 321 try: 322 subprocess.call(['cowsay', '--', DESCRIPTION]) 323 print("") 324 except (FileNotFoundError, PermissionError): 325 print(DESCRIPTION) 326 print("") 327 328 defines = {} 329 330 options = features(args, parser) 331 defines.update(build_type(args)) 332 defines.update(get_compiler(args, parser)) 333 defines.update(get_install_prefixes(args)) 334 335 bindir = bindir_creation(args, defines) 336 invoke_cmake(args, bindir, defines, options) 337 338 339def parse_args(): 340 """ argument parsing """ 341 342 cli = argparse.ArgumentParser( 343 description=DESCRIPTION, 344 epilog=EPILOG, 345 formatter_class=argparse.ArgumentDefaultsHelpFormatter) 346 347 cli.add_argument("--mode", "-m", 348 choices=["debug", "release"], 349 default=getenv("BUILDMODE", default="debug"), 350 help="controls cmake build mode") 351 cli.add_argument("--optimize", "-O", 352 choices=["auto", "0", "1", "g", "2", "3", "max"], 353 default=getenv("OPTIMIZE", default="auto"), 354 help=("controls optimization-related flags. " + 355 "is set according to mode if 'auto'. " + 356 "conflicts with --flags")) 357 cli.add_argument("--sanitize", 358 choices=["none", "yes", "mem", "thread"], 359 default=getenv("SANITIZER", default="none"), 360 help=("enable one of those (run-time) code sanitizers." 361 "'yes' enables the address and " 362 "undefined sanitizers.")) 363 cli.add_argument("--sanitize-fatal", action='store_true', 364 default=getenv_bool("SANITIZER_FATAL"), 365 help="With --sanitize, stop execution on first problem.") 366 cli.add_argument("--compiler", "-c", 367 default=getenv("CXX"), 368 help="c++ compiler executable, default=$ENV[CXX]") 369 cli.add_argument("--with", action='append', dest='with_', metavar='OPTION', 370 help="enable optional functionality. " 371 "for a list of available features, " 372 "use --list-options") 373 cli.add_argument("--without", action='append', metavar='OPTION', 374 help="disable optional functionality. " 375 "for a list of available features, " 376 "use --list-options") 377 cli.add_argument("--list-options", action="store_true", 378 help="list available optional feature switches") 379 cli.add_argument("--flags", "-f", 380 default=getenv("CXXFLAGS", "CCFLAGS", "CFLAGS"), 381 help="compiler flags") 382 cli.add_argument("--ldflags", "-l", 383 default=getenv("LDFLAGS"), 384 help="linker flags") 385 cli.add_argument("--prefix", "-p", default="/usr/local", 386 help="installation directory prefix") 387 cli.add_argument("--py-prefix", default=None, 388 help="python module installation directory prefix") 389 cli.add_argument("--dry-run", action='store_true', 390 help="just print the cmake invocation without callint it") 391 cli.add_argument("--cmake-binary", default="cmake", 392 help="path to the cmake binary") 393 cli.add_argument("--ninja", action="store_true", 394 help="use ninja instead of GNU make") 395 cli.add_argument("--ccache", action="store_true", 396 help="activate using the ccache compiler cache") 397 cli.add_argument("--clang-tidy", action="store_true", 398 help="emit clang-tidy analysis messages") 399 cli.add_argument("--download-nyan", action="store_true", 400 help="enable automatic download of the nyan project") 401 402 # arguments after -- are used as raw cmake args 403 cli.add_argument('raw_cmake_args', nargs=argparse.REMAINDER, default=[], 404 help="all args after ' -- ' are passed directly to cmake") 405 406 args = cli.parse_args() 407 408 if args.sanitize == 'none' and args.sanitize_fatal: 409 cli.error('--sanitize-fatal only valid with --sanitize') 410 411 if args.list_options: 412 header = "{} | Default state".format("Optional features:".ljust(25)) 413 print("{}\n{}".format(header, "-" * len(header))) 414 for option, state in sorted(OPTIONS.items()): 415 state_str = (state if not isinstance(state, bool) 416 else ("on" if state else "off")) 417 print("{} | {}".format(option.ljust(25), state_str)) 418 exit(0) 419 420 return args, cli 421 422 423if __name__ == "__main__": 424 main(*parse_args()) 425