Skip to content

Commit

Permalink
[issue1097] Modernize the CMake build system.
Browse files Browse the repository at this point in the history
We update our build system and now require CMake 3.16. In order to add new files
for compilation, they now have to be listed within a library in
src/search/CMakeLists.txt. SoPlex installations should now set the environment
variable soplex_DIR instead of DOWNWARD_SOPLEX_ROOT. The name of CMake options
changed from, e.g., PLUGIN_FF_HEURISTIC_ENABLED to LIBRARY_FF_HEURISTIC_ENABLED.
  • Loading branch information
FlorianPommerening authored Sep 15, 2023
1 parent 17ae77a commit 6b192cd
Show file tree
Hide file tree
Showing 22 changed files with 1,466 additions and 1,546 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
CPLEX_URL: ${{ secrets.CPLEX2211_LINUX_URL }}
DOWNWARD_CPLEX_ROOT: /home/runner/lib/ibm/ILOG/CPLEX_Studio2211/cplex
CPLEX_LIB: /home/runner/lib/ibm/ILOG/CPLEX_Studio2211/cplex/bin/x86-64_linux/libcplex2211.so
DOWNWARD_SOPLEX_ROOT: /home/runner/lib/soplex-6.0.3x
soplex_DIR: /home/runner/lib/soplex-6.0.3x
SOPLEX_LIB: /home/runner/lib/soplex-6.0.3x/lib/
SOPLEX_INCLUDE: /home/runner/lib/soplex-6.0.3x/include/
steps:
Expand Down Expand Up @@ -76,7 +76,7 @@ jobs:
cd ..
cmake -S soplex -B build
cmake --build build
cmake --install build --prefix "${DOWNWARD_SOPLEX_ROOT}"
cmake --install build --prefix "${soplex_DIR}"
rm -rf soplex build
- name: Compile planner
Expand Down
84 changes: 37 additions & 47 deletions build.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#!/usr/bin/env python3

import errno
import multiprocessing
import os
import subprocess
import sys
Expand All @@ -12,25 +11,18 @@
if not config.startswith("_")}
DEFAULT_CONFIG_NAME = CONFIGS.pop("DEFAULT")
DEBUG_CONFIG_NAME = CONFIGS.pop("DEBUG")

CMAKE = "cmake"
DEFAULT_MAKE_PARAMETERS = []
CMAKE_GENERATOR = None
if os.name == "posix":
MAKE = "make"
try:
num_cpus = multiprocessing.cpu_count()
except NotImplementedError:
pass
else:
DEFAULT_MAKE_PARAMETERS.append('-j{}'.format(num_cpus))
CMAKE_GENERATOR = "Unix Makefiles"
elif os.name == "nt":
MAKE = "nmake"
CMAKE_GENERATOR = "NMake Makefiles"
else:
print("Unsupported OS: " + os.name)
sys.exit(1)

try:
# Number of usable CPUs (Unix only)
NUM_CPUS = len(os.sched_getaffinity(0))
except AttributeError:
# Number of available CPUs as a fall-back (may be None)
NUM_CPUS = os.cpu_count()

def print_usage():
script_name = os.path.basename(__file__)
Expand All @@ -43,18 +35,16 @@ def print_usage():
configs.append(name + "\n " + " ".join(args))
configs_string = "\n ".join(configs)
cmake_name = os.path.basename(CMAKE)
make_name = os.path.basename(MAKE)
generator_name = CMAKE_GENERATOR.lower()
default_config_name = DEFAULT_CONFIG_NAME
debug_config_name = DEBUG_CONFIG_NAME
print("""Usage: {script_name} [BUILD [BUILD ...]] [--all] [--debug] [MAKE_OPTIONS]
print(f"""Usage: {script_name} [BUILD [BUILD ...]] [--all] [--debug] [MAKE_OPTIONS]
Build one or more predefined build configurations of Fast Downward. Each build
uses {cmake_name} to generate {generator_name} and then uses {make_name} to compile the
code. Build configurations differ in the parameters they pass to {cmake_name}.
By default, the build uses N threads on a machine with N cores if the number of
cores can be determined. Use the "-j" option for {cmake_name} to override this default
behaviour.
uses {cmake_name} to compile the code using {generator_name} . Build configurations
differ in the parameters they pass to {cmake_name}. By default, the build uses all
available cores if this number can be determined. Use the "-j" option for
{cmake_name} to override this default behaviour.
Build configurations
{configs_string}
Expand All @@ -64,7 +54,7 @@ def print_usage():
--help Print this message and exit.
Make options
All other parameters are forwarded to {make_name}.
All other parameters are forwarded to the build step.
Example usage:
./{script_name} # build {default_config_name} in #cores threads
Expand All @@ -73,7 +63,7 @@ def print_usage():
./{script_name} --debug # build {debug_config_name}
./{script_name} release debug # build release and debug configs
./{script_name} --all VERBOSE=true # build all build configs with detailed logs
""".format(**locals()))
""")


def get_project_root_path():
Expand All @@ -92,41 +82,41 @@ def get_src_path():
def get_build_path(config_name):
return os.path.join(get_builds_path(), config_name)

def try_run(cmd, cwd):
print('Executing command "{}" in directory "{}".'.format(" ".join(cmd), cwd))
def try_run(cmd):
print(f'Executing command "{" ".join(cmd)}"')
try:
subprocess.check_call(cmd, cwd=cwd)
subprocess.check_call(cmd)
except OSError as exc:
if exc.errno == errno.ENOENT:
print("Could not find '%s' on your PATH. For installation instructions, "
"see https://www.fast-downward.org/ObtainingAndRunningFastDownward." %
cmd[0])
print(f"Could not find '{cmd[0]}' on your PATH. For installation instructions, "
"see https://www.fast-downward.org/ObtainingAndRunningFastDownward.")
sys.exit(1)
else:
raise

def build(config_name, cmake_parameters, make_parameters):
print("Building configuration {config_name}.".format(**locals()))
def build(config_name, configure_parameters, build_parameters):
print(f"Building configuration {config_name}.")

build_path = get_build_path(config_name)
rel_src_path = os.path.relpath(get_src_path(), build_path)
try:
os.makedirs(build_path)
except OSError as exc:
if exc.errno == errno.EEXIST and os.path.isdir(build_path):
pass
else:
raise
generator_cmd = [CMAKE, "-S", get_src_path(), "-B", build_path]
if CMAKE_GENERATOR:
generator_cmd += ["-G", CMAKE_GENERATOR]
generator_cmd += configure_parameters
try_run(generator_cmd)

try_run([CMAKE, "-G", CMAKE_GENERATOR] + cmake_parameters + [rel_src_path],
cwd=build_path)
try_run([MAKE] + make_parameters, cwd=build_path)
build_cmd = [CMAKE, "--build", build_path]
if NUM_CPUS:
build_cmd += ["-j", f"{NUM_CPUS}"]
if build_parameters:
build_cmd += ["--"] + build_parameters
try_run(build_cmd)

print("Built configuration {config_name} successfully.".format(**locals()))
print(f"Built configuration {config_name} successfully.")


def main():
config_names = []
make_parameters = DEFAULT_MAKE_PARAMETERS
build_parameters = []
for arg in sys.argv[1:]:
if arg == "--help" or arg == "-h":
print_usage()
Expand All @@ -138,11 +128,11 @@ def main():
elif arg in CONFIGS:
config_names.append(arg)
else:
make_parameters.append(arg)
build_parameters.append(arg)
if not config_names:
config_names.append(DEFAULT_CONFIG_NAME)
for config_name in config_names:
build(config_name, CONFIGS[config_name], make_parameters)
build(config_name, CONFIGS[config_name], build_parameters)


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion build_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
release_no_lp = ["-DCMAKE_BUILD_TYPE=Release", "-DUSE_LP=NO"]
# USE_GLIBCXX_DEBUG is not compatible with USE_LP (see issue983).
glibcxx_debug = ["-DCMAKE_BUILD_TYPE=Debug", "-DUSE_LP=NO", "-DUSE_GLIBCXX_DEBUG=YES"]
minimal = ["-DCMAKE_BUILD_TYPE=Release", "-DDISABLE_PLUGINS_BY_DEFAULT=YES"]
minimal = ["-DCMAKE_BUILD_TYPE=Release", "-DDISABLE_LIBRARIES_BY_DEFAULT=YES"]

DEFAULT = "release"
DEBUG = "debug"
12 changes: 12 additions & 0 deletions misc/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# This file is read by pytest before running the tests to set them up.
# We use it to add the option '--suppress' to the tests that is used by
# memory leak tests to ignore compiler-specific false positives.

def pytest_addoption(parser):
parser.addoption("--suppress", action="append", default=[],
help="Suppression files used in test-memory-leaks.py.")

def pytest_generate_tests(metafunc):
if "suppression_files" in metafunc.fixturenames:
supression_files = list(metafunc.config.option.suppress)
metafunc.parametrize("suppression_files", [supression_files], scope='session')
119 changes: 71 additions & 48 deletions misc/tests/test-dependencies.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,86 @@
#! /usr/bin/env python3

import os
import re
import shutil
import subprocess
import sys

DIR = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.dirname(os.path.dirname(DIR))
DOWNWARD_FILES = os.path.join(REPO, "src", "search", "DownwardFiles.cmake")
TEST_BUILD_CONFIGS = os.path.join(REPO, "test_build_configs.py")
BUILD = os.path.join(REPO, "build.py")
BUILDS = os.path.join(REPO, "builds")
paths_to_clean = [TEST_BUILD_CONFIGS]


def clean_up(paths_to_clean):
print("\nCleaning up")
for path in paths_to_clean:
print("Removing {path}".format(**locals()))
if os.path.isfile(path):
os.remove(path)
if os.path.isdir(path):
shutil.rmtree(path)
print("Done cleaning")


with open(DOWNWARD_FILES) as d:
content = d.readlines()

content = [line for line in content if '#' not in line]
content = [line for line in content if 'NAME' in line or 'CORE_PLUGIN' in line or 'DEPENDENCY_ONLY' in line]

plugins_to_be_tested = []
for line in content:
if 'NAME' in line:
plugins_to_be_tested.append(line.replace("NAME", "").strip())
if 'CORE_PLUGIN' in line or 'DEPENDENCY_ONLY' in line:
plugins_to_be_tested.pop()

with open(TEST_BUILD_CONFIGS, "w") as f:
for plugin in plugins_to_be_tested:
lowercase = plugin.lower()
line = "{lowercase} = [\"-DCMAKE_BUILD_TYPE=Debug\", \"-DDISABLE_PLUGINS_BY_DEFAULT=YES\"," \
" \"-DPLUGIN_{plugin}_ENABLED=True\"]\n".format(**locals())
f.write(line)
paths_to_clean.append(os.path.join(BUILDS, lowercase))

plugins_failed_test = []
for plugin in plugins_to_be_tested:
LIBRARY_DEFINITION_FILE = os.path.join(REPO, "src", "search", "CMakeLists.txt")
BUILDS = os.path.join(REPO, "builds/test-dependencies")


# Find the closing bracket matching the opening bracket that occurred before start
def find_corresponding_closing_bracket(content, start):
nested_level = 1
for index, character in enumerate(content[start:]):
if character == "(":
nested_level += 1
if character == ")":
nested_level -= 1
if nested_level == 0:
return index+start

# Extract all "create_fast_downward_library(...)" blocks and if the library is
# not a core or depencency library, add its name to the return list.
def get_library_definitions(content):
libraries = []
library_definition_string = r"create_fast_downward_library\("
pattern = re.compile(library_definition_string)
for library_match in pattern.finditer(content):
start = library_match.start()+len(library_definition_string)
end = find_corresponding_closing_bracket(content, start)
library_definition = content[start:end]
# we cannot manually en-/disable core and dependency only libraries
if any(s in library_definition for s in ["CORE_LIBRARY", "DEPENDENCY_ONLY"]):
continue
name_match = re.search(r"NAME\s+(\S+)", library_definition)
assert name_match
name = name_match.group(1)
libraries.append(name)
return libraries


# Try to get the number of CPUs
try:
# Number of usable CPUs (Unix only)
NUM_CPUS = len(os.sched_getaffinity(0))
except AttributeError:
# Number of available CPUs as a fall-back (may be None)
NUM_CPUS = os.cpu_count()

# Read in the file where libraries are defined and extract the library names.
with open(LIBRARY_DEFINITION_FILE) as d:
content = d.read()
content = re.sub(r"#(.*)", "", content) # Remove all comments
libraries = get_library_definitions(content)

# Build each library and add it to libraries_failed_test if not successfull.
libraries_failed_test = []
for library in libraries:
build_path = os.path.join(BUILDS, library.lower())
config_cmd = [
"cmake", "-S", os.path.join(REPO, "src"), "-B", build_path,
"-DCMAKE_BUILD_TYPE=Debug", "-DDISABLE_LIBRARIES_BY_DEFAULT=YES",
f"-DLIBRARY_{library.upper()}_ENABLED=True"
]
build_cmd = ["cmake", "--build", build_path]
if NUM_CPUS:
build_cmd += ["-j", f"{NUM_CPUS}"]

try:
subprocess.check_call([BUILD, plugin.lower()])
subprocess.check_call(config_cmd)
subprocess.check_call(build_cmd)
except subprocess.CalledProcessError:
plugins_failed_test.append(plugin)
libraries_failed_test.append(library)

if plugins_failed_test:
# Report the result of the test.
if libraries_failed_test:
print("\nFailure:")
for plugin in plugins_failed_test:
print("{plugin} failed dependencies test".format(**locals()))
for library in libraries_failed_test:
print("{library} failed dependencies test".format(**locals()))
sys.exit(1)
else:
print("\nAll plugins have passed dependencies test")
clean_up(paths_to_clean)
print("\nAll libraries have passed dependencies test")
Loading

0 comments on commit 6b192cd

Please sign in to comment.