diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml index 79c0371ce..aa8bf715e 100644 --- a/.github/workflows/analyze.yml +++ b/.github/workflows/analyze.yml @@ -96,7 +96,7 @@ jobs: run: | build-wrapper-linux-x86-64 \ --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} \ - build/build.py \ + ./build/build.sh \ --compiler=${{ matrix.compiler }} \ --config=${{ matrix.configuration }} \ --test diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6ac7fece5..dc160e4ca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -84,7 +84,7 @@ jobs: - name: Build and test run: | # Build and test - build/build.py \ + ./build/build.sh \ --compiler=${{ matrix.compiler }} \ --config=${{ matrix.configuration }} \ --test @@ -160,7 +160,7 @@ jobs: - name: Build and test run: | # Build and test - build/build.py \ + ./build/build.sh \ --compiler=${{ matrix.compiler }} \ --config=${{ matrix.configuration }} \ --test diff --git a/.vscode/settings.json b/.vscode/settings.json index 15e95f13b..a1059786e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,9 +6,6 @@ "[cpp]": { "editor.defaultFormatter": "xaver.clang-format" }, - "flake8.args": [ - "max-line-length=80" - ], "cmake.configureOnOpen": false, "editor.formatOnSave": true, "editor.tabSize": 2, diff --git a/build/build.py b/build/build.py deleted file mode 100755 index 92e97c935..000000000 --- a/build/build.py +++ /dev/null @@ -1,251 +0,0 @@ -#!/usr/bin/env python3 -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # -# Part of the Tit Solver project, under the MIT License. -# See /LICENSE.md for license information. SPDX-License-Identifier: MIT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # - -import argparse -import os -import pathlib -import shutil -import subprocess -import sys - -HOME_DIR = os.path.expanduser("~") -SOURCE_DIR = os.path.abspath(".") -OUTPUT_DIR = os.path.join(SOURCE_DIR, "output", "cmake_output") -TEST_DIR = os.path.join(OUTPUT_DIR, "tests") - -CMAKE_EXE = "cmake" -CTEST_EXE = "ctest" - - -def parse_args(): - """Parse command line arguments.""" - parser = argparse.ArgumentParser(description=main.__doc__) - parser.add_argument( - "-c", - "--config", - metavar="", - help="build configuration", - action="store", - default="Release", - choices=["Debug", "Release", "Coverage"], - ) - parser.add_argument( - "-f", - "--force", - help="disable all static analysis during the build (for experimenting)", - action="store_true", - ) - parser.add_argument( - "-t", - "--test", - help="run tests after successfully building the project", - action="store_true", - ) - parser.add_argument( - "-j", - "--jobs", - metavar="", - help="number of threads to parallelize the build", - action="store", - nargs="?", - type=int, - const=0, - ) - parser.add_argument( - "--compiler", - metavar="", - help="override system's C++ compiler", - action="store", - default=None, - ) - parser.add_argument( - "--vcpkg-root", - metavar="", - help="vcpkg package manager installation root path", - action="store", - default=None, - ) - parser.add_argument( - "--no-vcpkg", - help="do not use vcpkg for building the project", - action="store_true", - ) - parser.add_argument( - "--dry", - help="perform a dry build: discard all previously built data.", - action="store_true", - ) - parser.add_argument( - "--args", - metavar="", - help="extra CMake configuration arguments", - dest="extra_args", - action="store", - nargs=argparse.REMAINDER, - default=None, - ) - return parser.parse_args() - - -def prepare_build_dir(dry=False): - """Prepare build directory.""" - if dry: - print("Performing a dry build.") - shutil.rmtree(OUTPUT_DIR, ignore_errors=True) - pathlib.Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True) - - -def find_vcpkg(hint=None): - """Find vcpkg installation""" - # Use our hint first, next search environment. - vcpkg_root = ( - hint - or os.environ.get("VCPKG_ROOT") - or os.environ.get("VCPKG_INSTALLATION_ROOT") - ) - # Next look for common directories. - if not vcpkg_root: - maybe_vcpkg_root = os.path.join(HOME_DIR, "vcpkg") - if os.path.exists(maybe_vcpkg_root) and os.path.isdir(maybe_vcpkg_root): - vcpkg_root = maybe_vcpkg_root - return vcpkg_root - - -def configure( - config, - force=False, - compiler=None, - no_vcpkg=False, - vcpkg_root=None, - extra_args=None, -): - """Configure the project.""" - cmake_args = [CMAKE_EXE] - # Setup the source and build directories. - cmake_args += ["-S", SOURCE_DIR, "-B", OUTPUT_DIR] - # Setup the build configuration. - assert config - print(f"Configuration: {config}.") - cmake_args += ["-D" f"CMAKE_BUILD_TYPE={config}"] - # Setup static analysis. - # (Should be always passed, since `-D...` options are cached). - if force: - print("'Force' build: static analysis is disabled.") - cmake_args += ["-D", f"SKIP_ANALYSIS={'YES' if force else 'NO'}"] - # Setup C++ compiler. - if compiler or os.environ.get("CXX"): - print(f"Overriding system's C++ compiler: {compiler}.") - # To override the system compiler, there are two available approaches: - # - # 1. Specify it using CMake's `CMAKE_CXX_COMPILER` variable. - # - # 2. Export it as an environment variable named `CXX`. - # - # The former method appears more favorable for pure CMake. However, there's - # an issue with this approach: the overridden compiler isn't recognized by - # vcpkg. To ensure vcpkg uses our designated compiler, we can create a - # custom triplet following these steps: - # - # 1. Create `my-triplet.toolchain` with the following content: - # - # set(CMAKE_CXX_COMPILER "my-favorite-compiler") - # - # 2. Create `my-triplet.cmake` with the following content: - # - # set(VCPKG_CHAINLOAD_TOOLCHAIN_FILE "/path/to/my-triplet.toolchain") - # set(VCPKG_CRT_LINKAGE dynamic) - # set(VCPKG_LIBRARY_LINKAGE static) - # - # 3. Provide the following parameters to CMake: - # - # -D VCPKG_OVERLAY_TRIPLETS=/path/to/triplet/and/toolchain/ - # -D VCPKG_HOST_TRIPLET=my-triplet - # -D VCPKG_TARGET_TRIPLET=my-triplet - # - # In my view, using custom triplets solely to switch the compiler might be - # excessive. Therefore, I'll stick to the environment variable method for now. - if compiler: - os.environ["CXX"] = compiler - # Setup vcpkg. - if not no_vcpkg: - ## Find vcpkg. - vcpkg_root = find_vcpkg(hint=vcpkg_root) - if not vcpkg_root: - print("Unable to find vcpkg.") - sys.exit(-1) - ## Locate toolchain file. - toolchain_path = os.path.join( - vcpkg_root, "scripts", "buildsystems", "vcpkg.cmake" - ) - if not os.path.exists(toolchain_path) or not os.path.isfile(toolchain_path): - print("Unable to find vcpkg toolchain file.") - sys.exit(-1) - cmake_args += ["-D", f"CMAKE_TOOLCHAIN_FILE={toolchain_path}"] - # Append extra arguments. - if extra_args: - cmake_args += extra_args - # Run CMake. - if (exit_code := subprocess.call(cmake_args)) != 0: - sys.exit(exit_code) - - -def build(config, jobs=0): - """Build project.""" - cmake_args = ["cmake"] - # Setup build directory. - cmake_args += ["--build", OUTPUT_DIR] - # Setup the build configuration. - assert config - cmake_args += ["--config", config] - # Setup the number of threads. - if jobs and jobs > 1: - print(f"Building with {jobs} threads.") - cmake_args += ["-j", jobs] - # Run CMake. - if (exit_code := subprocess.call(cmake_args)) != 0: - sys.exit(exit_code) - - -def test(jobs=0): - """Test project.""" - # Prepare CTest arguments. - ctest_args = ["ctest", "--output-on-failure"] - # Setup the number of threads. - if jobs and jobs > 1: - ctest_args += ["-j", jobs] - # Run CTest. - if (exit_code := subprocess.call(ctest_args, cwd=TEST_DIR)) != 0: - sys.exit(exit_code) - - -def main(): - """Build Tit.""" - # Parse command line arguments. - args = parse_args() - - # Prepare the build directory. - prepare_build_dir(dry=args.dry) - - # Configure. - configure( - config=args.config, - force=args.force, - compiler=args.compiler, - no_vcpkg=args.no_vcpkg, - vcpkg_root=args.vcpkg_root, - extra_args=args.extra_args, - ) - - # Build. - build(config=args.config, jobs=args.jobs) - - # Test. - if args.test: - test(jobs=args.jobs) - - -if __name__ == "__main__": - main() diff --git a/build/build.sh b/build/build.sh new file mode 100755 index 000000000..8fedec6c5 --- /dev/null +++ b/build/build.sh @@ -0,0 +1,246 @@ +#!/usr/bin/env bash +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # +# Part of the Tit Solver project, under the MIT License. +# See /LICENSE.md for license information. SPDX-License-Identifier: MIT +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # + +HOME_DIR=$HOME +SOURCE_DIR=$(pwd) +OUTPUT_DIR="$SOURCE_DIR/output/cmake_output" +TEST_DIR="$OUTPUT_DIR/tests" + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # + +COLUMNS=${COLUMNS:-$([ $TERM ] && tput cols || echo 80)} + +echo_banner() { + for _ in $(seq 1 $COLUMNS); do echo -n "~"; done + echo +} + +echo_thick_banner() { + for _ in $(seq 1 $COLUMNS); do echo -n "="; done + echo +} + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # + +CONFIG="Release" +FORCE=false +RUN_TESTS=false +JOBS=1 +COMPILER=$CXX +VCPKG_ROOT="" +NO_VCPKG=false +DRY=false +EXTRA_ARGS=() + +usage() { + echo "Usage: $(basename "$0") [options]" + echo "Options:" + echo " -h, --help Print this help message." + echo " -c, --config Build configuration (Debug/Release/Coverage, default: Release)." + echo " -f, --force Disable all static analysis during the build." + echo " -t, --test Run tests after successfully building the project." + echo " -j, --jobs Number of threads to parallelize the build." + echo "" + echo "Advanced options:" + echo " --compiler Override system's C++ compiler." + echo " --vcpkg-root Vcpkg package manager installation root path." + echo " --no-vcpkg Do not use vcpkg for building the project." + echo " --dry Perform a dry build: discard all previously built data." + echo " -- Extra CMake configuration arguments." + exit 1 +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + -h | -help | --help) + usage;; + -c | --config) + CONFIG="$2" + shift 2;; + --config=*) + CONFIG="${1#*=}" + shift 1;; + -f | --force) + FORCE=true + shift;; + -t | --test) + RUN_TESTS=true + shift;; + -j | --jobs) + JOBS="$2" + shift 2;; + --compiler) + COMPILER="$2" + shift 2;; + --compiler=*) + COMPILER="${1#*=}" + shift 1;; + --vcpkg-root) + VCPKG_ROOT="$2" + shift 2;; + --vcpkg-root=*) + VCPKG_ROOT="${1#*=}" + shift 2;; + --no-vcpkg) + NO_VCPKG=true + shift;; + --dry) + DRY=true + shift;; + --) + EXTRA_ARGS=("${@:2}") + break;; + *) + echo "Invalid argument: $1." + usage;; + esac + done + # Display parsed options. + echo "Options:" + [[ $CONFIG ]] && echo "* CONFIG: $CONFIG" + [[ $FORCE ]] && echo "* FORCE: $FORCE" + [[ $RUN_TESTS = true ]] && echo "* RUN_TESTS: YES" + [[ $JOBS -gt 1 ]] && echo "* JOBS: $JOBS" + [[ $COMPILER ]] && echo "* COMPILER: $COMPILER" + [[ $VCPKG_ROOT ]] && echo "* VCPKG_ROOT: $VCPKG_ROOT" + [[ $NO_VCPKG = true ]] && echo "* NO_VCPKG: YES" + [[ $DRY = true ]] && echo "* DRY: YES" + [[ $EXTRA_ARGS ]] && echo "* EXTRA_ARGS: $EXTRA_ARGS" +} + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # + +CMAKE_EXE="cmake" + +prepare_build_dir() { + if [ "$DRY" = true ]; then + echo "Performing a dry build." + rm -rf "$OUTPUT_DIR" + fi + mkdir -p "$OUTPUT_DIR" +} + +find_vcpkg() { + VCPKG_ROOT="${VCPKG_ROOT:-$VCPKG_INSTALLATION_ROOT}" + VCPKG_ROOT="${VCPKG_ROOT:-$HOME_DIR/vcpkg}" + if [ ! -d "$VCPKG_ROOT" ]; then + echo "Unable to find vcpkg." + exit 1 + fi +} + +# Function to configure the project +configure() { + CMAKE_ARGS=("$CMAKE_EXE") + CMAKE_ARGS+=("-S" "$SOURCE_DIR" "-B" "$OUTPUT_DIR") + CMAKE_ARGS+=("-D" "CMAKE_BUILD_TYPE=$CONFIG") + if [ "$FORCE" = true ]; then + echo "* 'Force' build: static analysis is disabled." + SKIP_ANALYSIS="YES" + else + SKIP_ANALYSIS="NO" + fi + CMAKE_ARGS+=("-D" "SKIP_ANALYSIS=$SKIP_ANALYSIS") + if [ -n "$COMPILER" ] || [ -n "$CXX" ]; then + echo "* Overriding system's C++ compiler: $COMPILER." + # To override the system compiler, there are two available approaches: + # + # 1. Specify it using CMake's `CMAKE_CXX_COMPILER` variable. + # + # 2. Export it as an environment variable named `CXX`. + # + # The former method appears more favorable for pure CMake. However, there's + # an issue with this approach: the overridden compiler isn't recognized by + # vcpkg. To ensure vcpkg uses our designated compiler, we can create a + # custom triplet following these steps: + # + # 1. Create `my-triplet.toolchain` with the following content: + # + # set(CMAKE_CXX_COMPILER "my-favorite-compiler") + # + # 2. Create `my-triplet.cmake` with the following content: + # + # set(VCPKG_CHAINLOAD_TOOLCHAIN_FILE "/path/to/my-triplet.toolchain") + # set(VCPKG_CRT_LINKAGE dynamic) + # set(VCPKG_LIBRARY_LINKAGE static) + # + # 3. Provide the following parameters to CMake: + # + # -D VCPKG_OVERLAY_TRIPLETS=/path/to/triplet/and/toolchain/ + # -D VCPKG_HOST_TRIPLET=my-triplet + # -D VCPKG_TARGET_TRIPLET=my-triplet + # + # In my view, using custom triplets solely to switch the compiler might be + # excessive. Therefore, I'll stick to the environment variable method for + # now. + export CXX="$COMPILER" + fi + if [ "$NO_VCPKG" != true ]; then + find_vcpkg + TOOLCHAIN_PATH="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" + if [ ! -f "$TOOLCHAIN_PATH" ]; then + echo "- Unable to find vcpkg toolchain file!" + exit 1 + fi + CMAKE_ARGS+=("-D" "CMAKE_TOOLCHAIN_FILE=$TOOLCHAIN_PATH") + fi + [ -n "$EXTRA_ARGS" ] && CMAKE_ARGS+=("$EXTRA_ARGS") + "${CMAKE_ARGS[@]}" || exit $? +} + +build() { + CMAKE_ARGS=("cmake") + CMAKE_ARGS+=("--build" "$OUTPUT_DIR") + CMAKE_ARGS+=("--config" "$CONFIG") + if [ "$JOBS" -gt 1 ]; then + echo "* Building with $JOBS threads." + CMAKE_ARGS+=("-j" "$JOBS") + fi + "${CMAKE_ARGS[@]}" || exit $? +} + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # + +CTEST_EXE="ctest" + +run_tests() { + CTEST_ARGS=("$CTEST_EXE" "--output-on-failure") + if [ "$JOBS" -gt 1 ]; then + echo "* Running tests with $JOBS threads." + CTEST_ARGS+=("-j" "$JOBS") + fi + (cd "$TEST_DIR"; "${CTEST_ARGS[@]}") || exit $? +} + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # + +main() { + START_TIME=$(date +%s.%N) + echo_thick_banner + echo "Running build script." + parse_args "$@" + echo_banner + echo "Configuring..." + prepare_build_dir + configure + echo_banner + echo "Building..." + build + if [ "$RUN_TESTS" = true ]; then + echo_banner + echo "Running tests..." + run_tests + fi + echo_thick_banner + END_TIME=$(date +%s.%N) + ELAPSED=$(echo "$END_TIME - $START_TIME" | bc -l) + printf "All done. Elapsed: %ss.\n" $ELAPSED +} + +main "$@" + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #