You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
mpack/test/unit/configure.py

617 lines
22 KiB
Python

#!/usr/bin/env python3
# Copyright (c) 2015-2021 Nicholas Fraser and the MPack authors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# This is the buildsystem configuration tool for the MPack unit test suite. It
# tests the compiler for support for various flags and features and generates a
# ninja build file to build the unit test suite in a variety of configurations.
#
# It can be run with a GCC-style compiler or with MSVC. To run it with MSVC,
# you must first have the Visual Studio build tools on your path. This means
# you need to either open a Visual Studio Build Tools command prompt, or source
# vsvarsall.bat for some version of the Visual Studio Build Tools.
import shutil, os, sys, subprocess
from os import path
globalbuild = path.join(".build", "unit")
os.makedirs(globalbuild, exist_ok=True)
###################################################
# Determine Compiler
###################################################
cc = None
compiler = "unknown"
if os.getenv("CC"):
cc = os.getenv("CC")
elif shutil.which("cl.exe"):
cc = "cl"
else:
cc = "cc"
if not shutil.which(cc):
raise Exception("Compiler cannot be found!")
if cc.lower() == "cl" or cc.lower() == "cl.exe":
compiler = "MSVC"
elif cc.endswith("cproc"):
compiler = "cproc"
elif cc.endswith("chibicc"):
compiler = "chibicc"
elif cc.endswith("8cc"):
compiler = "8cc"
else:
# try --version
ret = subprocess.run([cc, "--version"], universal_newlines=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if ret.returncode == 0:
if ret.stdout.startswith("cparser "):
compiler = "cparser"
elif "clang" in ret.stdout:
compiler = "Clang"
if compiler == "unknown":
# try -v
ret = subprocess.run([cc, "-v"], universal_newlines=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if ret.returncode == 0:
for line in (ret.stdout + "\n" + ret.stderr).splitlines():
if line.startswith("tcc "):
compiler = "TinyCC"
break
elif line.startswith("gcc "):
compiler = "GCC"
break
print("Using " + compiler + " compiler with executable: " + cc)
if compiler == "MSVC":
obj_extension = ".obj"
exe_extension = ".exe"
else:
obj_extension = ".o"
exe_extension = ""
msvc = (compiler == "MSVC")
###################################################
# Compiler Probing
###################################################
config = {
"flags": {},
}
flagtest_src = path.join(globalbuild, "flagtest.c")
flagtest_exe = path.join(globalbuild, "flagtest" + exe_extension)
with open(flagtest_src, "w") as out:
out.write("""
int main(int argc, char** argv) {
// array dereference to test for the existence of
// sanitizer libs when using -fsanitize (libubsan)
// compare it to another string in the array so that
// -Wzero-as-null-pointer-constant works
return argv[argc - 1] == argv[0];
}
""")
def checkFlags(flags):
if isinstance(flags, str):
flags = [flags,]
configArg = "|".join(flags)
if configArg in config["flags"]:
return config["flags"][configArg]
print("Testing flag(s): " + " ".join(flags) + " ... ", end="")
sys.stdout.flush()
if msvc:
cmd = [cc, "/WX"] + flags + [flagtest_src, "/Fe" + flagtest_exe]
else:
cmd = [cc, "-Werror"] + flags + [flagtest_src, "-o", flagtest_exe]
ret = subprocess.run(cmd, universal_newlines=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if ret.returncode == 0:
print("Supported.")
supported = True
else:
print("Not supported.")
supported = False
config["flags"][configArg] = supported
return supported
def flagsIfSupported(flags):
if checkFlags(flags):
if isinstance(flags, str):
return [flags]
return flags
return []
# We use -Og for all debug builds if we have it, but ONLY under GCC. It can
# sometimes improve warnings, and things run a lot faster especially under
# Valgrind, but Clang stupidly maps it to -O1 which has some optimizations
# that break debugging!
hasOg = False
print("Testing flag(s): -Og ... ", end="")
sys.stdout.flush()
if msvc:
print("Not supported.")
else:
ret = subprocess.run([cc, "-v"], universal_newlines=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
sys.stdout.flush()
if ret.returncode != 0:
print("Not supported.")
else:
for line in ret.stdout.splitlines():
if line.startswith("gcc version"):
hasOg = True
break
if hasOg:
print("Supported.")
else:
print("May be supported, but we won't use it.")
###################################################
# Common Flags
###################################################
global_cppflags = []
if msvc:
global_cppflags += [
"/W4", "/WX",
# debug to PDB with synchronous writes since we're doing parallel builds
# (we specify a per-build PDB path during build generation below)
"/Zi", "/FS"
]
else:
global_cppflags += [
"-Wall", "-Wextra", "-Werror",
"-Wconversion", "-Wundef",
"-Wshadow", "-Wcast-qual",
"-g",
]
global_cppflags += [
"-Isrc", "-Itest/unit/src",
"-DMPACK_VARIANT_BUILDS=1",
"-DMPACK_HAS_CONFIG=1",
]
defaultfeatures = [
"-DMPACK_READER=1",
"-DMPACK_WRITER=1",
"-DMPACK_EXPECT=1",
"-DMPACK_NODE=1",
]
allfeatures = defaultfeatures + [
"-DMPACK_COMPATIBILITY=1",
"-DMPACK_EXTENSIONS=1",
]
noioconfigs = [
"-DMPACK_STDLIB=1",
"-DMPACK_MALLOC=test_malloc",
"-DMPACK_FREE=test_free",
]
allconfigs = noioconfigs + [
"-DMPACK_STDIO=1",
]
# optimization
if msvc:
debugflags = ["/Od", "/MDd"]
releaseflags = ["/O2", "/MD"]
else:
debugflags = [hasOg and "-Og" or "-O0"]
releaseflags = ["-O2"]
debugflags.append("-DDEBUG")
releaseflags.append("-DNDEBUG")
# flags for specifying source language
cxxlinkflags = []
if msvc:
cflags = ["/TC"]
cxxflags = ["/TP", "/EHsc"]
else:
cflags = [
checkFlags("-std=c11") and "-std=c11" or "-std=c99",
"-Wc++-compat"
]
cxxflags = [
"-x", "c++",
"-Wmissing-declarations",
]
# When building as C++ on macOS, clang will emit calls to std::terminate
# even if no C++ features are used so libc++ is required. We link with cc,
# not c++ so we need to link it manually, and there is apparently no way to
# link it statically either. (This overrides the use of libstdc++ if libc++
# is installed so it's not ideal. We can worry about this later.)
if checkFlags(cxxflags + ["-lc++"]):
cxxlinkflags.append("-lc++")
###################################################
# Variable Flags
###################################################
if not os.getenv("CI") and not msvc:
# we have to force color diagnostics to get color output from ninja
# (ninja will strip the colors if it's being piped)
if checkFlags("-fdiagnostics-color=always"):
global_cppflags.append("-fdiagnostics-color=always")
elif checkFlags("-fcolor-diagnostics=always"):
global_cppflags.append("-color-diagnostics=always")
if checkFlags("-Wstrict-aliasing=3"):
global_cppflags.append("-Wstrict-aliasing=3")
elif checkFlags("-Wstrict-aliasing=2"):
global_cppflags.append("-Wstrict-aliasing=2")
elif checkFlags("-Wstrict-aliasing"):
global_cppflags.append("-Wstrict-aliasing")
extra_warnings_to_test = [
"-Wpedantic",
"-Wmissing-variable-declarations",
"-Wfloat-conversion",
]
if not msvc:
extra_warnings_to_test += ["-fstrict-aliasing"]
for flag in extra_warnings_to_test:
global_cppflags += flagsIfSupported(flag)
cflags += flagsIfSupported("-Wmissing-prototypes")
cflags += flagsIfSupported("-Wstrict-prototypes")
#TODO
ltoflags = []
###################################################
# Build configuration
###################################################
builds = {}
class Build:
def __init__(self, name, cppflags, ldflags):
self.name = name
self.cppflags = cppflags
self.ldflags = ldflags
self.run_wrapper = None
self.exclude = False
def addBuild(name, cppflags, ldflags=[]):
builds[name] = Build(name, cppflags[:], ldflags[:])
def addDebugReleaseBuilds(name, cppflags, ldflags = []):
addBuild(name + "-debug", cppflags + debugflags, ldflags)
addBuild(name + "-release", cppflags + releaseflags, ldflags)
addDebugReleaseBuilds('default', defaultfeatures + allconfigs + cflags)
addDebugReleaseBuilds('everything', allfeatures + allconfigs + cflags)
addDebugReleaseBuilds('empty', allconfigs + cflags)
addDebugReleaseBuilds('reader', ["-DMPACK_READER=1"] + allconfigs + cflags)
addDebugReleaseBuilds('expect', ["-DMPACK_READER=1", "-DMPACK_EXPECT=1"] + allconfigs + cflags)
addDebugReleaseBuilds('node', ["-DMPACK_NODE=1"] + allconfigs + cflags)
addDebugReleaseBuilds('compatibility', defaultfeatures + ["-DMPACK_COMPATIBILITY=1"] + allconfigs + cflags)
addDebugReleaseBuilds('extensions', defaultfeatures + ["-DMPACK_EXTENSIONS=1"] + allconfigs + cflags)
addDebugReleaseBuilds('no-float', allfeatures + allconfigs + cflags + ["-DMPACK_FLOAT=0"])
addDebugReleaseBuilds('no-double', allfeatures + allconfigs + cflags + ["-DMPACK_DOUBLE=0"])
# writer builds
addDebugReleaseBuilds('writer-only',
["-DMPACK_WRITER=1", "-DMPACK_BUILDER=0"]
+ allconfigs + cflags)
addDebugReleaseBuilds('builder-internal',
["-DMPACK_WRITER=1", "-DMPACK_BUILDER=1", "-DMPACK_BUILDER_INTERNAL_STORAGE=1"]
+ allconfigs + cflags)
addDebugReleaseBuilds('builder-nointernal',
["-DMPACK_WRITER=1", "-DMPACK_BUILDER=1", "-DMPACK_BUILDER_INTERNAL_STORAGE=0"]
+ allconfigs + cflags)
# no i/o
addDebugReleaseBuilds('noio', allfeatures + noioconfigs + cflags)
addDebugReleaseBuilds('noio-writer', ["-DMPACK_WRITER=1"] + noioconfigs + cflags)
addDebugReleaseBuilds('noio-reader', ["-DMPACK_READER=1"] + noioconfigs + cflags)
addDebugReleaseBuilds('noio-expect', ["-DMPACK_READER=1", "-DMPACK_EXPECT=1"] + noioconfigs + cflags)
addDebugReleaseBuilds('noio-node', ["-DMPACK_NODE=1"] + noioconfigs + cflags)
# embedded builds without libc (using builtins)
addDebugReleaseBuilds('embed', allfeatures + cflags)
addDebugReleaseBuilds('embed-writer', ["-DMPACK_WRITER=1"] + cflags)
addDebugReleaseBuilds('embed-reader', ["-DMPACK_READER=1"] + cflags)
addDebugReleaseBuilds('embed-expect', ["-DMPACK_READER=1", "-DMPACK_EXPECT=1"] + cflags)
addDebugReleaseBuilds('embed-node', ["-DMPACK_NODE=1"] + cflags)
addDebugReleaseBuilds('embed-nobuiltins', ["-DMPACK_NO_BUILTINS=1"] + allfeatures + cflags)
haveValgrind = shutil.which("valgrind")
if haveValgrind:
# use valgrind for everything build if available
builds["everything-debug"].run_wrapper = "valgrind"
builds["everything-release"].run_wrapper = "valgrind"
# language versions
if msvc:
addDebugReleaseBuilds('c++', allfeatures + allconfigs + cxxflags)
elif compiler != "TinyCC":
# MPack is really C11 code with C++ support. We need lots of compiler
# extensions to build as ANSI C. We technically only support gnu89 so we
# need to disable pedantic C89 warnings. We add
# -Wdeclaration-after-statement even though MPack mixes declarations and
# code just to make sure MPack disables the warning properly.
gnu89flags = ["-std=gnu89", "-Wno-pedantic", "-Wdeclaration-after-statement"]
if checkFlags(gnu89flags):
addDebugReleaseBuilds('gnu89', allfeatures + allconfigs + gnu89flags)
if checkFlags("-std=c11"):
# if we're using c11 for everything else, we still need to test c99
addDebugReleaseBuilds('c99', allfeatures + allconfigs + ["-std=c99"])
for version in ["c++11", "gnu++11", "c++14", "c++17"]:
flags = cxxflags + ["-std=" + version]
if checkFlags(flags):
addDebugReleaseBuilds(version, allfeatures + allconfigs + flags, cxxlinkflags)
# Make sure C++11 compiles with disabled features (see #66)
cxx11flags = cxxflags + ["-std=c++11"]
if checkFlags(cxx11flags):
addDebugReleaseBuilds('c++11-empty', allconfigs + cxx11flags, cxxlinkflags)
# We disable pedantic in C++98 due to our use of variadic macros, trailing
# commas, ll format specifiers, and probably more. We technically only support
# C++98 with those extensions.
cxx98flags = cxxflags + ["-std=c++98"]
if checkFlags("-Wno-pedantic"):
cxx98flags += ["-Wno-pedantic"]
addDebugReleaseBuilds('c++98', allfeatures + allconfigs + cxx98flags, cxxlinkflags)
# 32-bit builds
if not msvc and checkFlags("-m32"):
addDebugReleaseBuilds('m32', allfeatures + allconfigs + cflags + ["-m32"], ["-m32"])
addDebugReleaseBuilds('cxx98-m32', allfeatures + allconfigs + cxx98flags + ["-m32"], ["-m32"])
if checkFlags(cxx11flags):
addDebugReleaseBuilds('c++11-m32', allfeatures + allconfigs + cxx11flags + ["-m32"], ["-m32"])
# lto build
if msvc:
addBuild('lto', allfeatures + allconfigs + cflags + releaseflags + ["/GL"], ["/LTCG"])
elif compiler != "TinyCC":
ltoflags = ["-O3", "-flto", "-fuse-linker-plugin", "-fno-fat-lto-objects"]
if checkFlags(ltoflags):
ltoflags = allfeatures + allconfigs + cflags + ltoflags
else:
ltoflags = ["-O3", "-flto"]
if checkFlags(ltoflags):
ltoflags = allfeatures + allconfigs + cflags + ltoflags
else:
ltoflags = None
if ltoflags:
addBuild('lto', ltoflags, ltoflags)
if haveValgrind:
builds["lto"].run_wrapper = "valgrind"
# size-optimized build (both debug and release)
if msvc:
sizeOptimize = ["/O1", "/MD"]
else:
sizeOptimize = ["-Os"]
addBuild('optimize-size-debug', allfeatures + allconfigs + cflags + sizeOptimize +
["-DMPACK_OPTIMIZE_FOR_SIZE=1", "-DMPACK_STRINGS=0", "-DDEBUG"])
addBuild('optimize-size-release', allfeatures + allconfigs + cflags + sizeOptimize +
["-DMPACK_OPTIMIZE_FOR_SIZE=1", "-DMPACK_STRINGS=0", "-DNDEBUG"])
# miscellaneous special builds
addBuild('notrack', allfeatures + allconfigs + cflags + debugflags + ["-DMPACK_NO_TRACKING=1"])
addDebugReleaseBuilds('realloc', allfeatures + allconfigs + cflags + ["-DMPACK_REALLOC=test_realloc"])
if not msvc and compiler != "TinyCC":
addBuild('O3', allfeatures + allconfigs + cflags + ["-O3"])
if haveValgrind:
builds["O3"].run_wrapper = "valgrind"
addBuild('fastmath', allfeatures + allconfigs + cflags + ["-ffast-math"])
if haveValgrind:
builds["fastmath"].run_wrapper = "valgrind"
addBuild('coverage', allfeatures + allconfigs + cflags +
["-DMPACK_GCOV=1", "--coverage", "-fno-inline", "-fno-inline-small-functions", "-fno-default-inline"],
["--coverage"])
builds["coverage"].exclude = True # don't run coverage during "all". run separately by CI.
if hasOg:
addBuild('O0', allfeatures + allconfigs + cflags + ["-DDEBUG", "-O0"])
# sanitizers
if msvc:
# https://devblogs.microsoft.com/cppblog/asan-for-windows-x64-and-debug-build-support/
# /INFERASANLIBS is enabled by default, no need to specify them anymore
addDebugReleaseBuilds('sanitize-address', allfeatures + allconfigs + cflags + ["/fsanitize=address"])
elif compiler != "TinyCC":
def addSanitizerBuilds(name, cppflags, ldflags=[]):
if checkFlags(cppflags):
addDebugReleaseBuilds(name, allfeatures + allconfigs + cflags + cppflags, ldflags)
addSanitizerBuilds('sanitize-undefined', ["-fsanitize=undefined"], ["-fsanitize=undefined"])
addSanitizerBuilds('sanitize-safe-stack', ["-fsanitize=safe-stack"], ["-fsanitize=safe-stack"])
addSanitizerBuilds('sanitize-address', ["-fsanitize=address"], ["-fsanitize=address"])
addSanitizerBuilds('sanitize-memory', ["-fsanitize=memory"], ["-fsanitize=memory"])
# not technically a sanitizer, but close enough:
addSanitizerBuilds('sanitize-stack-protector', ["-Wstack-protector", "-fstack-protector-all"])
###################################################
# Ninja generation
###################################################
srcs = []
for paths in [path.join("src", "mpack"), path.join("test", "unit", "src")]:
for root, dirs, files in os.walk(paths):
for name in files:
if name.endswith(".c"):
srcs.append(os.path.join(root, name))
ninja = path.join(globalbuild, "build.ninja")
with open(ninja, "w") as out:
out.write("# This file is auto-generated.\n")
out.write("# Do not edit it; your changes will be erased.\n")
out.write("\n")
# 1.3 for gcc deps, 1.1 for pool
out.write("ninja_required_version = 1.3\n")
out.write("\n")
out.write("rule compile\n")
if msvc:
out.write(" command = " + cc + " /showIncludes $flags /c $in /Fo$out\n")
out.write(" deps = msvc\n")
else:
out.write(" command = " + cc + " " + "-MD -MF $out.d $flags -c $in -o $out\n")
out.write(" deps = gcc\n")
out.write(" depfile = $out.d\n")
out.write("\n")
out.write("rule link\n")
if msvc:
out.write(" command = link $flags $in /OUT:$out\n")
else:
out.write(" command = " + cc + " $flags $in -o $out\n")
out.write("\n")
# unfortunately right now the unit tests all try to write to the same files,
# so they break when run concurrently. we need to make it write to files under
# that config's build/ folder; for now we just run them sequentially.
out.write("pool run_pool\n")
out.write(" depth = 1\n")
out.write("run_wrapper =\n")
out.write("rule run\n")
out.write(" command = $run_wrapper$in\n")
out.write(" pool = run_pool\n")
out.write("\n")
out.write("rule help\n")
out.write(" command = cat .build/help\n")
out.write("build help: help\n")
out.write("\n")
for buildname in sorted(builds.keys()):
build = builds[buildname]
buildfolder = path.join(globalbuild, buildname)
cppflags = global_cppflags + build.cppflags
ldflags = build.ldflags
objs = []
if msvc:
# Specify a per-build PDB path so that we don't try to link at the
# same time a PDB file is being written
cppflags.append("/Fd" + buildfolder)
ldflags.append("/DEBUG")
for src in srcs:
obj = path.join(buildfolder, "objs", src[:-2] + obj_extension)
objs.append(obj)
out.write("build " + obj + ": compile " + src + "\n")
out.write(" flags = " + " ".join(cppflags) + "\n")
runner = path.join(buildfolder, "runner") + exe_extension
out.write("build " + runner + ": link " + " ".join(objs) + "\n")
out.write(" flags = " + " ".join(ldflags) + "\n")
# You can omit "run-" in front of any build to just build it without
# running it. This lets you run it some other way (e.g. under gdb,
# with/without Valgrind, etc.)
out.write("build " + buildname + ": phony " + runner + "\n\n")
out.write("build run-" + buildname + ": run " + runner + "\n")
if build.run_wrapper:
run_wrapper = build.run_wrapper
out.write(" run_wrapper = " + run_wrapper + " ")
if run_wrapper == "valgrind":
out.write("--leak-check=full --error-exitcode=1 ")
out.write("--suppressions=tools/valgrind-suppressions ")
out.write("--show-leak-kinds=all --errors-for-leak-kinds=all ")
out.write("\n")
out.write("default run-everything-debug\n")
out.write("build default: phony run-everything-debug\n")
out.write("\n")
# Builds included under the "more" target
more = [
"run-default-debug",
"run-everything-debug",
"run-everything-release",
"run-embed-debug",
"run-embed-release",
"run-no-float-release",
]
if "gnu89-debug" in builds:
more += [
"run-gnu89-debug",
"run-gnu89-release",
]
if "c++11-debug" in builds:
more += [
"run-c++11-debug",
]
out.write("build more: phony " + " ".join(more))
if ltoflags:
out.write(" run-lto")
out.write("\n\n")
out.write("build all: phony")
for build in sorted(builds.keys()):
if not builds[build].exclude:
out.write(" run-")
out.write(build)
out.write("\n")
print("Generated " + ninja)
with open(path.join(globalbuild, "help"), "w") as out:
out.write("\n")
out.write("Available targets:\n")
out.write("\n")
out.write(" (default)\n")
out.write(" more\n")
out.write(" all\n")
out.write(" clean\n")
out.write(" help\n")
out.write("\n")
for build in sorted(builds.keys()):
out.write(" run-" + build + "\n")
out.close()