diff --git a/BUILD_OPTIONS.md b/BUILD_OPTIONS.md index 6fc62a5b64..1840613b9c 100644 --- a/BUILD_OPTIONS.md +++ b/BUILD_OPTIONS.md @@ -17,6 +17,15 @@ For a more complete list of cmake variables, take a look in the file `CMakeCache # Ditch GAMBIT components that you don't intend to use: itch -Ditch="ColliderBit;NeutrinoBit;Mathematica" +# Select only the Bits you need: Bits +# Similar to -Ditch, but opt-in rather than opt-out. If some Bits +# are specified using -DBits, all other Bits except ScannerBit are +# added to the ditch list. -DBits and -Ditch must not overlap. +-DBits="DarkBit;PrecisionBit;SpecBit;DecayBit" # typical dark matter project +-DBits="ColliderBit;PrecisionBit;SpecBit;DecayBit" # typical collider project +-DBits="CosmoBit;DarkBit" # typical cosmology project + + # List the FlexibleSUSY models to build: BUILD_FS_MODELS # The names of the available FlexibleSUSY models correspond to # the subdirectories in @@ -87,5 +96,16 @@ For a more complete list of cmake variables, take a look in the file `CMakeCache # Create Graphviz files: HAVE_GRAPHVIZ (On|Off) -DHAVE_GRAPHVIZ=On + +# Skip the build of any backend interface that no enabled Bit references +# at the source level: GAMBIT_TRIM_BACKEND_INTERFACES (On|Off) +# Auto-set to ON when -DBits is used or when -Ditch removes any Bit. +# Set this to OFF to disable the trimming explicitly, e.g. when developing +# a new backend that no Bit yet references. Pair with +# -DGAMBIT_FORCE_BACKEND_INTERFACE="Name1;Name2" to keep specific backends +# regardless. +-DGAMBIT_TRIM_BACKEND_INTERFACES=On +-DGAMBIT_FORCE_BACKEND_INTERFACE="Acropolis;DarkCast" + ``` diff --git a/Backends/include/gambit/Backends/frontends/simplexs_1_0.hpp b/Backends/include/gambit/Backends/frontends/simple_xs_1_0.hpp similarity index 100% rename from Backends/include/gambit/Backends/frontends/simplexs_1_0.hpp rename to Backends/include/gambit/Backends/frontends/simple_xs_1_0.hpp diff --git a/Backends/scripts/backend_harvester.py b/Backends/scripts/backend_harvester.py index 01afbdc0f6..7f37c7de3d 100644 --- a/Backends/scripts/backend_harvester.py +++ b/Backends/scripts/backend_harvester.py @@ -66,16 +66,23 @@ def main(argv): backend_type_headers = set([]) bossed_backend_type_headers = set([]) exclude_backends=set([]) + enabled_bits=set([]) + force_keep_backends=set([]) # Handle command line options verbose = False + verbose_build = False try: - opts, args = getopt.getopt(argv,"vx:",["verbose","exclude-backends="]) + opts, args = getopt.getopt(argv,"vx:", + ["verbose","exclude-backends=","bits=","force-backends=","verbose-build"]) except getopt.GetoptError: print('Usage: backend_harvestor.py [flags]') print(' flags:') print(' -v : More verbose output') print(' -x backend1,backend2,... : Exclude backend1, backend2, etc.') + print(' --bits=B1,B2,... : Restrict the build to dependencies of these Bits.') + print(' --force-backends=A,B,... : Keep these backends, even if no enabled Bit needs them.') + print(' --verbose-build : Print which frontends were auto-excluded.') sys.exit(2) for opt, arg in opts: if opt in ('-v','--verbose'): @@ -83,17 +90,39 @@ def main(argv): print('backend_harvester.py: verbose=True') elif opt in ('-x','--exclude-backends'): exclude_backends.update(neatsplit(",",arg)) - - # Get list of frontend header files to include in backend_rollcall.hpp - frontend_headers.update(retrieve_generic_headers(verbose,"./Backends/include/gambit/Backends/frontends","frontend",exclude_backends)) - frontend_headers_excluded.update(retrieve_generic_headers(verbose,"./Backends/include/gambit/Backends/frontends","frontend",exclude_backends, retrieve_excluded=True)) - # Get list of backend type header files + elif opt == '--bits': + enabled_bits.update(b for b in neatsplit(",",arg) if b) + elif opt == '--force-backends': + force_keep_backends.update(b for b in neatsplit(",",arg) if b) + elif opt == '--verbose-build': + verbose_build = True + + # If --bits is given, drop frontends that no enabled Bit references. + auto_excluded = set() + if enabled_bits: + auto_excluded, used_backends, all_backends = derive_optin_backend_excludes( + enabled_bits, ".", + "./Backends/include/gambit/Backends/frontends", + force_keep_backends) + exclude_backends.update(auto_excluded) + if verbose_build or verbose: + print("backend_harvester.py: enabled Bits = {0}".format(sorted(enabled_bits))) + if force_keep_backends: + print("backend_harvester.py: force-kept backends = {0}".format(sorted(force_keep_backends))) + print("backend_harvester.py: auto-excluded {0} backend(s) (no enabled Bit references them):".format(len(auto_excluded))) + for be in sorted(auto_excluded): + print(" - {0}".format(be)) + + # Discover backend type headers up front. Backends with BOSSed type + # headers must remain available so their type definitions stay in the + # rollcall, even if the user (or the auto-trim) tried to exclude them. backend_type_headers.update(retrieve_generic_headers(verbose,"./Backends/include/gambit/Backends/backend_types","backend type",set([]))) bossed_backend_type_headers.update(retrieve_generic_headers(verbose,"./Backends/include/gambit/Backends/backend_types","BOSSed type",set([]))) - # Remove bossed backends from list of excluded backends exclude_backends = set([be for be in exclude_backends if not any([excluded(bossed_be, [be]) for bossed_be in bossed_backend_type_headers])]) - # Get list of frontend header files to include in backend_rollcall.hpp + + # Partition the frontend headers using the final exclude set. frontend_headers.update(retrieve_generic_headers(verbose,"./Backends/include/gambit/Backends/frontends","frontend",exclude_backends)) + frontend_headers_excluded.update(retrieve_generic_headers(verbose,"./Backends/include/gambit/Backends/frontends","frontend",exclude_backends, retrieve_excluded=True)) if verbose: print("Frontend headers identified:") @@ -210,7 +239,7 @@ def main(argv): print("Generated backend_types_rollcall.hpp.\n") import yaml - with open("./config/gambit_backends.yaml", "w+") as f: + with open("./config/gambit_backend_interfaces.yaml", "w+") as f: yaml.dump({ "enabled": extract_yaml_for_diagnostic(frontend_headers), "disabled": extract_yaml_for_diagnostic(frontend_headers_excluded) diff --git a/Backends/scripts/list_backends.py b/Backends/scripts/list_backends.py new file mode 100644 index 0000000000..f5d8f34bca --- /dev/null +++ b/Backends/scripts/list_backends.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +"""Print a status table (one row per canonical BACKENDNAME) for every known +GAMBIT backend interface. Reads config/gambit_backend_interfaces.yaml plus +a data file written by CMakeLists.txt at configure time.""" +import io +import os +import sys + +import yaml + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_REPO = os.path.normpath(os.path.join(_HERE, "..", "..")) +sys.path.insert(0, os.path.join(_REPO, "Utils", "scripts")) + +from harvesting_tools import ( # noqa: E402 + compute_bits_per_backend, + get_all_backendnames, +) +import list_table # noqa: E402 + + +def main(): + if len(sys.argv) != 2: + sys.stderr.write("Usage: list_backends.py \n") + sys.exit(2) + + active_targets = [] + build_dir = "" + ditched_set = set() + with io.open(sys.argv[1], "r") as f: + for line in f: + if line.startswith("TARGETS="): + active_targets = [s for s in line[len("TARGETS="):].rstrip("\n").split(";") if s] + elif line.startswith("BUILD_DIR="): + build_dir = line[len("BUILD_DIR="):].rstrip("\n").strip() + elif line.startswith("DITCHED="): + ditched_set = {s for s in line[len("DITCHED="):].rstrip("\n").split(";") if s} + + def is_ditched(canonical): + # Match canonical (or its underscore-collapsed form, for cases like + # SUSY_HIT vs susyhit_1.5) against any cmake ${itch} entry, with or + # without a trailing _. Catches BOSSed backends that the + # interfaces YAML keeps "enabled" for type availability after the + # build itself was excluded. + cl = canonical.lower() + cl_collapsed = canonical.replace('_', '').lower() + for entry in ditched_set: + e = entry.lower() + if e == cl or e.startswith(cl + "_"): + return True + if cl_collapsed != cl and (e == cl_collapsed or e.startswith(cl_collapsed + "_")): + return True + return False + + def is_target_installed(target): + if not build_dir or not target: + return False + return os.path.exists(os.path.join( + build_dir, target + "-prefix", "src", + target + "-stamp", target + "-done")) + + with io.open(os.path.join(_REPO, "config", "gambit_backend_interfaces.yaml"), "r") as f: + bdata = yaml.safe_load(f) or {} + enabled_set = set((bdata.get("enabled") or {}).keys()) + + frontend_dir = os.path.join(_REPO, "Backends", "include", "gambit", "Backends", "frontends") + all_backends = get_all_backendnames(frontend_dir) + bits_per = compute_bits_per_backend(_REPO, frontend_dir) + + # Group each target under its longest-matching canonical (so e.g. + # darksusy_MSSM_6.4.0 lands under DarkSUSY_MSSM, not under DarkSUSY). + canonicals_by_len = sorted(all_backends, key=len, reverse=True) + targets_by_canonical = {b: [] for b in all_backends} + for t in active_targets: + tl = t.lower() + matched = False + for canonical in canonicals_by_len: + if tl.startswith(canonical.lower() + "_"): + targets_by_canonical[canonical].append(t) + matched = True + break + # Fallback for canonicals whose make target drops internal underscores + # (SUSY_HIT -> susyhit_1.5). + if not matched: + for canonical in canonicals_by_len: + cl = canonical.replace("_", "").lower() + if cl != canonical.lower() and tl.startswith(cl + "_"): + targets_by_canonical[canonical].append(t) + break + + rows = [] + for canonical in sorted(all_backends, key=lambda s: s.lower()): + targets = sorted(targets_by_canonical[canonical]) + active = (canonical in enabled_set) and not is_ditched(canonical) + bits = bits_per.get(canonical, []) + rows.append((canonical, active, bits, targets)) + + name_w = max(len(r[0]) for r in rows) + name_w = max(name_w, len("Backend")) + bits_w = max(len(", ".join(r[2]) if r[2] else "none") for r in rows) + # "used by: " column spans the literal prefix (9 chars) plus the padded + # bits string. Bump to fit the "Used by Bits" header if narrower. + used_by_col_w = max(9 + bits_w, len("Used by Bits")) + bits_w = used_by_col_w - 9 + print(" {bold}{h1:<{nw}} {h2:<{tw}} {h3:<{w3}} {h4}{reset}".format( + bold=list_table.BOLD, reset=list_table.RESET, + h1="Backend", nw=name_w, + h2="Status", tw=list_table.TAG_W, + h3="Used by Bits", w3=used_by_col_w, + h4="Make targets")) + print(" {0} {1} {2} {3}".format( + "-" * name_w, "-" * list_table.TAG_W, + "-" * used_by_col_w, "-" * len("Make targets"))) + + for name, active, bits, targets in rows: + if bits: + bits_visible = ", ".join(bits) + bits_field = bits_visible + " " * (bits_w - len(bits_visible)) + else: + bits_field = list_table.DIM + "none" + list_table.RESET + " " * (bits_w - len("none")) + + if not targets: + targets_field = list_table.DIM + "none" + list_table.RESET + else: + ann = [] + for t in targets: + if is_target_installed(t): + ann.append("{} [installed]".format(t)) + else: + ann.append(t) + targets_field = ", ".join(ann) + + if not active: + kind = "disabled" + elif targets and any(is_target_installed(t) for t in targets): + kind = "installed" + elif targets: + kind = "not_installed" + else: + kind = "" + print(" {name:<{nw}} {tag} used by: {bits} targets: {tgts}".format( + name=name, nw=name_w, tag=list_table.tag_for_kind(kind), + bits=bits_field, tgts=targets_field)) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Backends/src/frontends/simple_xs_1_0.cpp b/Backends/src/frontends/simple_xs_1_0.cpp index 290b6b48c3..7d7f6d80c8 100644 --- a/Backends/src/frontends/simple_xs_1_0.cpp +++ b/Backends/src/frontends/simple_xs_1_0.cpp @@ -16,7 +16,7 @@ /// ********************************************* #include "gambit/Backends/frontend_macros.hpp" -#include "gambit/Backends/frontends/simplexs_1_0.hpp" +#include "gambit/Backends/frontends/simple_xs_1_0.hpp" BE_INI_FUNCTION {} END_BE_INI_FUNCTION diff --git a/CMakeLists.txt b/CMakeLists.txt index 91767f2481..a551bb56e1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -553,7 +553,9 @@ endif() add_custom_target(docs WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} COMMAND doxygen doc/doxygen.conf) # Work out which modules to include in the compile +gambit_translate_bits_into_itch() retrieve_bits(GAMBIT_BITS ${PROJECT_SOURCE_DIR} "${itch}" "Loud") +gambit_configure_optin_build() # Set up targets to make standalone tarballs of the different modules add_standalone_tarballs("${GAMBIT_BITS}" "${GAMBIT_VERSION_FULL}") @@ -629,6 +631,14 @@ list(REMOVE_ITEM MODULE_HARVESTER_FILES "${PROJECT_SOURCE_DIR}/ScannerBit//inclu list(APPEND MODULE_HARVESTER_FILES "${PROJECT_SOURCE_DIR}/config/resolution_type_equivalency_classes.yaml") set(MODULE_HARVESTER_FILES ${MODULE_HARVESTER_FILES} ${BACKEND_HARVESTER_FILES}) remove_build_files(models_harvested backends_harvested modules_harvested printers_harvested colliders_harvested) + +# Add backends and scanners. Done before the harvest registration so late +# additions to ${itch} (e.g. the FeynHiggs gfortran>=10 ditch) reach the +# harvester. +include(cmake/externals.cmake) + +string (REPLACE ";" "," itch_with_commas "${itch}") + if(EXISTS "${PROJECT_SOURCE_DIR}/Elements/") add_gambit_custom(module_harvest modules_harvested MODULE_HARVESTER MODULE_HARVESTER_FILES ${itch_with_commas}) endif() @@ -652,14 +662,11 @@ endif() # Generate the CMakeLists.txt files for GAMBIT modules, Backends, Models and Printers) message("${Yellow}-- Updating GAMBIT module, model, backend, and printer CMake files.${ColourReset}") -set(update_cmakelists ${PROJECT_SOURCE_DIR}/cmake/scripts/update_cmakelists.py -x __not_a_real_name__,${itch_with_commas}) +set(update_cmakelists ${PROJECT_SOURCE_DIR}/cmake/scripts/update_cmakelists.py -x __not_a_real_name__,${itch_with_commas} ${update_cmakelists_extra_args}) execute_process(RESULT_VARIABLE result COMMAND ${Python3_EXECUTABLE} ${update_cmakelists} WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}) check_result(${result} ${update_cmakelists}) message("${Yellow}-- Updating GAMBIT module, backend, and printer CMake files - done.${ColourReset}") -# Add backends and scanners -include(cmake/externals.cmake) - # Add GAMBIT subdirectories. add_subdirectory(Logs) add_subdirectory(Utils) @@ -699,3 +706,23 @@ include(cmake/executables.cmake) if(EXISTS "${PROJECT_SOURCE_DIR}/ScannerBit/") include(${PROJECT_BINARY_DIR}/linkedout.cmake) endif() + +# `make list-backends` and `make list-scanners` status-table targets. +# Each writes its data file on every cmake configure. +file(WRITE "${PROJECT_BINARY_DIR}/list_backends_data.txt" + "TARGETS=${GAMBIT_AVAILABLE_BACKEND_TARGETS}\nBUILD_DIR=${PROJECT_BINARY_DIR}\nDITCHED=${itch}\n") +add_custom_target(list-backends + COMMAND ${Python3_EXECUTABLE} ${PROJECT_SOURCE_DIR}/Backends/scripts/list_backends.py ${PROJECT_BINARY_DIR}/list_backends_data.txt + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + USES_TERMINAL) +add_dependencies(list-backends backend_harvest) + +file(WRITE "${PROJECT_BINARY_DIR}/list_scanners_data.txt" + "EXTERNAL_TARGETS=${GAMBIT_AVAILABLE_SCANNER_TARGETS}\nPYTHON_SCANNERS=${GAMBIT_PYTHON_SCANNER_STATUS}\nBUILD_DIR=${PROJECT_BINARY_DIR}\n") +add_custom_target(list-scanners + COMMAND ${Python3_EXECUTABLE} ${PROJECT_SOURCE_DIR}/ScannerBit/scripts/list_scanners.py ${PROJECT_BINARY_DIR}/list_scanners_data.txt + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + USES_TERMINAL) + +message("${BoldYellow}-- Run `make list-backends` to see the available backend make targets.${ColourReset}") +message("${BoldYellow}-- Run `make list-scanners` to see the available scanner make targets.${ColourReset}") diff --git a/Core/src/diagnostics.cpp b/Core/src/diagnostics.cpp index bcb027f46f..b1c769465e 100644 --- a/Core/src/diagnostics.cpp +++ b/Core/src/diagnostics.cpp @@ -90,9 +90,9 @@ namespace Gambit void gambit_core::backend_diagnostic() { - YAML::Node gambit_backends_yaml = YAML::LoadFile(GAMBIT_DIR "/config/gambit_backends.yaml"); - auto gambit_backends = gambit_backends_yaml["enabled"].as>>(); - auto gambit_backends_disabled = gambit_backends_yaml["disabled"].as>>(); + YAML::Node backend_interfaces_yaml = YAML::LoadFile(GAMBIT_DIR "/config/gambit_backend_interfaces.yaml"); + auto gambit_backends = backend_interfaces_yaml["enabled"].as>>(); + auto gambit_backends_disabled = backend_interfaces_yaml["disabled"].as>>(); for (auto &backend : gambit_backends_disabled) { diff --git a/ScannerBit/scripts/list_scanners.py b/ScannerBit/scripts/list_scanners.py new file mode 100644 index 0000000000..4114881f2e --- /dev/null +++ b/ScannerBit/scripts/list_scanners.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +"""Print a status table of every known GAMBIT scanner plugin (one row per +canonical plugin name). Reads scratch/build_time/scanbit_excluded_libs.yaml +plus a data file written by CMakeLists.txt at configure time, and walks +ScannerBit/src/scanners/ for native scanner_plugin() registrations.""" +import io +import os +import re +import sys + +import yaml + + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_REPO = os.path.normpath(os.path.join(_HERE, "..", "..")) +sys.path.insert(0, os.path.join(_REPO, "Utils", "scripts")) + +import list_table # noqa: E402 + + +def parse_data_file(path): + external_targets = [] + python_scanners = [] + build_dir = "" + with io.open(path, "r") as f: + for line in f: + if line.startswith("EXTERNAL_TARGETS="): + external_targets = [s for s in line[len("EXTERNAL_TARGETS="):].rstrip("\n").split(";") if s] + elif line.startswith("PYTHON_SCANNERS="): + python_scanners = [s for s in line[len("PYTHON_SCANNERS="):].rstrip("\n").split(";") if s] + elif line.startswith("BUILD_DIR="): + build_dir = line[len("BUILD_DIR="):].rstrip("\n").strip() + return external_targets, python_scanners, build_dir + + +def is_target_installed(build_dir, target): + if not build_dir or not target: + return False + stamp = os.path.join(build_dir, target + "-prefix", "src", + target + "-stamp", target + "-done") + return os.path.exists(stamp) + + +def is_scanner_lib_built(libname): + return os.path.exists(os.path.join( + _REPO, "ScannerBit", "lib", "lib" + libname + ".so")) + + +def parse_excluded_yaml(path): + """Return {libname: [reason, ...]} for every excluded scanner/objective + library in scanbit_excluded_libs.yaml.""" + excluded = {} + if not os.path.exists(path): + return excluded + with io.open(path, "r") as f: + data = yaml.safe_load(f) or {} + for libfile, info in data.items(): + name = libfile + if name.startswith("lib"): + name = name[3:] + if name.endswith(".so"): + name = name[:-3] + raw_reasons = info.get("reason") or [] + if isinstance(raw_reasons, str): + raw_reasons = [raw_reasons] + # ScannerBit emits each reason as a literal YAML entry like + # '- file missing: "ROOT"', which yaml.safe_load parses as a + # single-key mapping; flatten back to "key: value". + reasons = [] + for r in raw_reasons: + if isinstance(r, dict): + for k, v in r.items(): + reasons.append("{}: {}".format(str(k).strip(), str(v).strip())) + elif r is not None: + s = str(r).strip() + if s: + reasons.append(s) + excluded[name] = reasons + return excluded + + +_PLUGIN_RE = re.compile( + r"scanner_plugin\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*,\s*version\s*\(\s*([^)]*)\)\s*\)" +) + + +def parse_native_plugins(scanners_dir): + """Walk ScannerBit/src/scanners//* and return a list of + {libname, plugin, version} dicts derived from scanner_plugin(...) + registrations. Skips python/ — those come from a separate data source.""" + rows = [] + if not os.path.isdir(scanners_dir): + return rows + for libdir in sorted(os.listdir(scanners_dir)): + libpath = os.path.join(scanners_dir, libdir) + if not os.path.isdir(libpath): + continue + if libdir == "python": + continue + libname = "scanner_" + libdir + for root, _dirs, files in os.walk(libpath): + for fname in files: + if not fname.endswith((".cpp", ".cc", ".cxx", ".hpp", ".h")): + continue + with io.open(os.path.join(root, fname), "r", errors="replace") as fh: + text = fh.read() + for m in _PLUGIN_RE.finditer(text): + name = m.group(1) + parts = [p.strip() for p in m.group(2).split(",") if p.strip()] + ver = ".".join(parts) + rows.append({"libname": libname, "plugin": name, "version": ver}) + return rows + + +def main(): + if len(sys.argv) != 2: + sys.stderr.write("Usage: list_scanners.py \n") + sys.exit(2) + + external_targets, python_scanner_lines, build_dir = parse_data_file(sys.argv[1]) + excluded = parse_excluded_yaml( + os.path.join(_REPO, "scratch", "build_time", "scanbit_excluded_libs.yaml") + ) + native = parse_native_plugins(os.path.join(_REPO, "ScannerBit", "src", "scanners")) + + # Map "" -> "scanner_" plus a versionless fallback for + # canonical dirs that drop the version (e.g. ScannerBit/src/scanners/great). + # Versionless slot is first-seen wins, which is fine because in practice + # only one scanner family hits this fallback. + target_for_lib = {} + for tgt in external_targets: + target_for_lib.setdefault("scanner_" + tgt, tgt) + if "_" in tgt: + target_for_lib.setdefault("scanner_" + tgt.rsplit("_", 1)[0], tgt) + + grouped = {} + for r in native: + libname = r["libname"] + target = target_for_lib.get(libname, "") + grouped.setdefault(r["plugin"], []).append({ + "version": r["version"], + "library": libname, + "target": target, + "disabled": libname in excluded, + "reason": "; ".join(excluded.get(libname, [])), + }) + + # [installed] = the user's build action for this scanner kind has + # completed: external scanners → ExternalProject stamp present; + # native scanners → libscanner_.so built. + def _ver_key(v): + return [int(c) if c.isdigit() else c + for c in re.split('([0-9]+)', v["version"])] + + grouped_rows = [] + for name in sorted(grouped, key=str.lower): + versions = sorted(grouped[name], key=_ver_key) + all_disabled = all(v["disabled"] for v in versions) + external_versions = [v for v in versions if v["target"]] + native_versions = [v for v in versions if not v["target"]] + + def is_version_ready(v): + if v["disabled"]: + return False + if v["target"]: + return is_target_installed(build_dir, v["target"]) + return is_scanner_lib_built(v["library"]) + + any_ready = any(is_version_ready(v) for v in versions) + if all_disabled: + kind = "disabled" + elif any_ready: + kind = "installed" + else: + kind = "not_installed" + + parts = [] + if external_versions: + tgt_strs = [] + for v in external_versions: + if v["disabled"] and not all_disabled: + tgt_strs.append("{} [disabled]".format(v["target"])) + elif is_version_ready(v): + tgt_strs.append("{} [installed]".format(v["target"])) + else: + tgt_strs.append(v["target"]) + parts.append("targets: " + ", ".join(tgt_strs)) + if native_versions and not external_versions: + body = "none; native GAMBIT scanner" + if kind == "not_installed": + body += " – build GAMBIT to install" + parts.append("targets: " + list_table.DIM + body + list_table.RESET) + info = " ".join(parts) + + grouped_rows.append((name, kind, info)) + + # Python rows: one per scanner. Status is libscanner_python lib status (if + # excluded, all are disabled) overlaid with per-scanner Python module + # availability. + py_lib_disabled = "scanner_python" in excluded + py_lib_built = is_scanner_lib_built("scanner_python") + python_rows = [] + for line in python_scanner_lines: + parts = line.split("|") + if len(parts) < 2: + continue + pname = parts[0] + pstatus = parts[1] + # Restore "+" → ", " in the missing-pkgs field (see python_scanners.cmake). + pmissing = parts[2].replace("+", ", ") if len(parts) > 2 else "" + if py_lib_disabled or pstatus != "enabled": + kind = "disabled" + elif py_lib_built: + kind = "installed" + else: + kind = "not_installed" + python_rows.append((pname, kind, pmissing)) + + all_names = [r[0] for r in grouped_rows] + [r[0] for r in python_rows] + if not all_names: + print(" (no scanner plugins found)") + return + name_w = max(max(len(n) for n in all_names), len("Scanner")) + + print(" {bold}{h1:<{nw}} {h2:<{tw}} {h3}{reset}".format( + bold=list_table.BOLD, reset=list_table.RESET, + h1="Scanner", nw=name_w, + h2="Status", tw=list_table.TAG_W, + h3="Make targets")) + print(" {0} {1} {2}".format( + "-" * name_w, "-" * list_table.TAG_W, "-" * len("Make targets"))) + + # Print grouped (native+external) rows. + for name, kind, info in grouped_rows: + print(" {n:<{nw}} {tag} {info}".format( + n=name, nw=name_w, tag=list_table.tag_for_kind(kind), info=info)) + + # Print Python rows (sorted alphabetically for parity with the grouped section). + for name, kind, missing in sorted(python_rows, key=lambda r: r[0].lower()): + if kind == "disabled": + if missing: + body = "none; python plugin – install package(s) [{}] to enable".format(missing) + else: + body = "none; python plugin – install package(s) to enable" + elif kind == "not_installed": + body = "none; python plugin – all packages available, build GAMBIT to install" + else: + body = "none; python plugin" + info = "targets: " + list_table.DIM + body + list_table.RESET + print(" {n:<{nw}} {tag} {info}".format( + n=name, nw=name_w, tag=list_table.tag_for_kind(kind), info=info)) + + +if __name__ == "__main__": + main() diff --git a/Utils/scripts/harvesting_tools.py b/Utils/scripts/harvesting_tools.py index 18df7dcee0..9ccf5e7262 100644 --- a/Utils/scripts/harvesting_tools.py +++ b/Utils/scripts/harvesting_tools.py @@ -686,6 +686,176 @@ def retrieve_generic_headers(verbose, starting_dir, kind, excludes, exclude_list return headers +# Helpers for the opt-in build path (-Dbits / GAMBIT_AUTO_TRIM_BACKENDS). +_SOURCE_EXTS = (".cpp", ".cc", ".c", ".hpp", ".hh", ".h") + +def derive_backendname_from_filename(stem): + """Derive a backend name from a frontend filename stem by dropping version-number tokens (e.g. DarkSUSY_MSSM_6_4_0 -> DarkSUSY_MSSM).""" + return "_".join(p for p in stem.split("_") if p and not p[0].isdigit()) + + +def get_all_backendnames(frontend_dir): + """Return the set of distinct BACKENDNAMEs declared by frontend headers, with a filename fallback for BOSSed frontends.""" + backends = set() + if not os.path.isdir(frontend_dir): + return backends + define_re = re.compile(r'^\s*#\s*define\s+BACKENDNAME\s+(\S+)', re.MULTILINE) + for fn in os.listdir(frontend_dir): + if not fn.endswith(".hpp"): continue + path = os.path.join(frontend_dir, fn) + try: + with io.open(path, "r", encoding="utf-8", errors="replace") as f: + text = f.read() + except (IOError, OSError): + continue + m = define_re.search(text) + if m: + backends.add(m.group(1)) + else: + backends.add(derive_backendname_from_filename(fn[:-len(".hpp")])) + return backends + + +def derive_used_backends(enabled_bits, project_root, frontend_dir): + """Return the backends used by any enabled Bit, derived from the same + rollcall-macro analysis that compute_bits_per_backend uses (no + token-sweeping over comments / string literals).""" + if not enabled_bits: + return set() + bits_per_backend = compute_bits_per_backend(project_root, frontend_dir) + enabled = set(enabled_bits) + return {b for b, bits in bits_per_backend.items() if any(bit in enabled for bit in bits)} + + +def derive_optin_backend_excludes(enabled_bits, project_root, + frontend_dir, force_keep_backends=None): + """Return (auto_excluded, used_backends, all_backends) for the opt-in build path.""" + force_keep = set(force_keep_backends or []) + all_backends = get_all_backendnames(frontend_dir) + used = derive_used_backends(enabled_bits, project_root, frontend_dir) + used |= force_keep + auto_excluded = all_backends - used + return auto_excluded, used, all_backends + + +# Bit attribution that can't be derived statically from rollcall macros: +# DDCalc — DarkBit references its capabilities via LONG_BACKEND_REQ +# with names assembled by C-preprocessor token pasting. +# HepLikeData — pure-data backend, no rollcall-macro reference at all. +_KNOWN_BIT_USES_OVERRIDES = { + "DDCalc": ["DarkBit"], + "HepLikeData": ["FlavBit"], +} + +_FRONTEND_SHARED_INCLUDE_RE = re.compile( + r'^\s*#\s*include\s+"(gambit/Backends/frontends/shared_includes/[^"]+)"', + re.MULTILINE) + +def _read_frontend_recursive(path, backends_inc_dir, _seen=None): + """Read a frontend .hpp with comments stripped, inlining any + shared_includes/ headers it pulls in (e.g. MicrOmegas singlet variants + share BE_* declarations through a common file).""" + if _seen is None: _seen = set() + if path in _seen or not os.path.exists(path): + return "" + _seen.add(path) + try: + with io.open(path, "r", encoding="utf-8", errors="replace") as f: + text = comment_remover(f.read()) + except (IOError, OSError): + return "" + out = text + for m in _FRONTEND_SHARED_INCLUDE_RE.finditer(text): + sub_path = os.path.join(backends_inc_dir, m.group(1)) + out += "\n" + _read_frontend_recursive(sub_path, backends_inc_dir, _seen) + return out + +_BE_MACRO_RE = re.compile(r'\bBE_(?:FUNCTION|VARIABLE|CONV_FUNCTION)\s*\(') + +def _extract_be_capabilities(text): + # Capability is the last quoted-string arg of each BE_* macro; nested + # parens (e.g. C++ argument-type lists) preclude a flat regex. + caps = set() + for m in _BE_MACRO_RE.finditer(text): + depth, i = 1, m.end() + while i < len(text) and depth > 0: + if text[i] == '(': depth += 1 + elif text[i] == ')': depth -= 1 + i += 1 + quotes = re.findall(r'"([^"]+)"', text[m.end():i-1]) + if quotes: + caps.add(quotes[-1]) + return caps + +_BIT_REQ_RE = re.compile(r'\b(?:LONG_)?BACKEND_REQ(?:_FROM_GROUP)?\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)') +_BIT_NCF_RE = re.compile(r'\bNEEDS_CLASSES_FROM\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)') + + +def compute_bits_per_backend(project_root, frontend_dir): + """Return {canonical BACKENDNAME -> sorted list of Bit names that depend + on it}, derived from rollcall macros: a Bit is attributed to a backend + if the Bit's rollcall headers contain (LONG_)BACKEND_REQ for any + capability the backend's frontend declares via BE_FUNCTION/VARIABLE/ + CONV_FUNCTION (following shared_includes), or NEEDS_CLASSES_FROM(). + _KNOWN_BIT_USES_OVERRIDES patches in cases the macros can't express.""" + all_backends = get_all_backendnames(frontend_dir) + if not all_backends: + return {} + + # Frontend headers can pull in shared includes via paths rooted at + # Backends/include — derive that root rather than assuming a layout. + backends_inc_dir = frontend_dir + while backends_inc_dir and os.path.basename(backends_inc_dir) != "include": + parent = os.path.dirname(backends_inc_dir) + if parent == backends_inc_dir: + break + backends_inc_dir = parent + + caps_by_backend = {b: set() for b in all_backends} + if os.path.isdir(frontend_dir): + for fn in sorted(os.listdir(frontend_dir)): + if not fn.endswith(".hpp"): + continue + backend = derive_backendname_from_filename(fn[:-len(".hpp")]) + if backend not in all_backends: + continue + text = _read_frontend_recursive( + os.path.join(frontend_dir, fn), backends_inc_dir) + caps_by_backend[backend] |= _extract_be_capabilities(text) + + # Substring match on 'Bit' is loose but harmless: only Bit dirs ever + # carry rollcall headers with the macros we look for. + bit_dirs = sorted(d for d in os.listdir(project_root) + if 'Bit' in d and os.path.isdir(os.path.join(project_root, d))) + result = {b: set() for b in all_backends} + for bit in bit_dirs: + inc_dir = os.path.join(project_root, bit, "include") + if not os.path.isdir(inc_dir): + continue + bit_caps, bit_ncfs = set(), set() + for root, _dirs, files in os.walk(inc_dir): + for fn in files: + if not fn.endswith((".hpp", ".h", ".hh")): + continue + try: + with io.open(os.path.join(root, fn), "r", + encoding="utf-8", errors="replace") as f: + text = comment_remover(f.read()) + except (IOError, OSError): + continue + for m in _BIT_REQ_RE.finditer(text): bit_caps.add(m.group(1)) + for m in _BIT_NCF_RE.finditer(text): bit_ncfs.add(m.group(1)) + for be in all_backends: + if (caps_by_backend[be] & bit_caps) or (be in bit_ncfs): + result[be].add(bit) + + for be, bits in _KNOWN_BIT_USES_OVERRIDES.items(): + if be in result: + result[be].update(bits) + + return {b: sorted(s) for b, s in result.items()} + + def same(f1, f2): """Check whether or not two files differ in their contents except for the date line""" file1 = open(f1, "r") diff --git a/Utils/scripts/list_table.py b/Utils/scripts/list_table.py new file mode 100644 index 0000000000..e0a2d29015 --- /dev/null +++ b/Utils/scripts/list_table.py @@ -0,0 +1,40 @@ +"""Shared formatting helpers for `make list-backends` / `make list-scanners`. +ANSI codes carry no visible width, so callers that pad table columns around +make_tag()/tag_for_kind() must compute padding from the visible labels.""" +import sys + + +_use_color = sys.stdout.isatty() + +YELLOW = "\033[33m" if _use_color else "" +GREEN = "\033[32m" if _use_color else "" +CYAN = "\033[36m" if _use_color else "" +DIM = "\033[2m" if _use_color else "" +BOLD = "\033[1m" if _use_color else "" +RESET = "\033[0m" if _use_color else "" + +# Status column width: fits the longest label, "[not installed]". +TAG_W = len("[not installed]") + +_TAG_LABEL = { + "disabled": "[disabled]", + "installed": "[installed]", + "not_installed": "[not installed]", + "": "", +} +_TAG_COLOR = { + "disabled": YELLOW, + "installed": GREEN, + "not_installed": CYAN, + "": "", +} + + +def make_tag(label, color): + if not label: + return " " * TAG_W + return color + label + RESET + " " * (TAG_W - len(label)) + + +def tag_for_kind(kind): + return make_tag(_TAG_LABEL[kind], _TAG_COLOR[kind]) diff --git a/cmake/backends.cmake b/cmake/backends.cmake index 4b6cef00e3..2f827fe5f5 100644 --- a/cmake/backends.cmake +++ b/cmake/backends.cmake @@ -2569,8 +2569,8 @@ if(NOT ditched_${name}_${ver}) endif() -# simplexs -set(name "simplexs") +# simple_xs +set(name "simple_xs") set(ver "1.0") set(dl "https://github.com/GambitBSM/gambit_simplexs/archive/refs/heads/main.zip") set(dir "${PROJECT_SOURCE_DIR}/Backends/examples/simple_xs/1.0/") diff --git a/cmake/cleaning.cmake b/cmake/cleaning.cmake index 35f6b04255..0f9b80a249 100644 --- a/cmake/cleaning.cmake +++ b/cmake/cleaning.cmake @@ -65,13 +65,16 @@ foreach(bit ${ALL_GAMBIT_BITS}) set(clean_files ${clean_files} "${PROJECT_SOURCE_DIR}/${bit}/examples/standalone_functors.cpp") endforeach() -#Arrange for removal of other scanner-related generated files upon "make clean". +# Arrange for removal of other scanner-related generated files upon "make clean". if(EXISTS "${PROJECT_SOURCE_DIR}/ScannerBit/") set(clean_files ${clean_files} "${PROJECT_BINARY_DIR}/linkedout.cmake") set(clean_files ${clean_files} "${PROJECT_SOURCE_DIR}/scratch/build_time/scanbit_reqd_entries.yaml") set(clean_files ${clean_files} "${PROJECT_SOURCE_DIR}/scratch/build_time/scanbit_flags.yaml") endif() +# Arrange for removal of other generated files upon "make clean". +set(clean_files ${clean_files} "${PROJECT_SOURCE_DIR}/config/gambit_backend_interfaces.yaml") + # Add all the clean files set_directory_properties(PROPERTIES ADDITIONAL_MAKE_CLEAN_FILES "${clean_files}") diff --git a/cmake/externals.cmake b/cmake/externals.cmake index e51f468c38..a6602e022e 100644 --- a/cmake/externals.cmake +++ b/cmake/externals.cmake @@ -158,6 +158,20 @@ macro(add_extra_targets type package ver dir dl target) endif() + # Track available backend make targets for the list-backends helper. + if (${type} STREQUAL "backend") + list(APPEND GAMBIT_AVAILABLE_BACKEND_TARGETS "${package}_${ver}") + elseif (${type} STREQUAL "backend model") + list(APPEND GAMBIT_AVAILABLE_BACKEND_TARGETS "${package}_${model}_${ver}") + elseif (${type} STREQUAL "backend base (functional alone)") + list(APPEND GAMBIT_AVAILABLE_BACKEND_TARGETS "${package}_${ver}") + endif() + + # Track available scanner make targets for the list-scanners helper. + if (${type} STREQUAL "scanner") + list(APPEND GAMBIT_AVAILABLE_SCANNER_TARGETS "${package}_${ver}") + endif() + #Add extra targets common to everything. enable_auto_rebuild(${pname}) set_target_properties(${pname} PROPERTIES EXCLUDE_FROM_ALL 1) @@ -195,12 +209,11 @@ function(check_ditch_status name version dir) set (itch "${itch}" "${name}_${version}") endif() endforeach() + string(TOLOWER "${name}_${version}" _nv_lower) foreach(ditch_command ${itch}) - execute_process(COMMAND ${Python3_EXECUTABLE} -c "print(\"${name}_${version}\".lower().startswith(\"${ditch_command}\".lower()))" - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - RESULT_VARIABLE result - OUTPUT_VARIABLE output) - if (output STREQUAL "True\n") + string(TOLOWER "${ditch_command}" _dc_lower) + string(FIND "${_nv_lower}" "${_dc_lower}" _loc) + if (_loc EQUAL 0) if(NOT ditched_${name}_${ver}) set(ditched_${name}_${version} TRUE) set(ditched_${name}_${version} TRUE PARENT_SCOPE) diff --git a/cmake/python_scanners.cmake b/cmake/python_scanners.cmake index 055c5fbdf7..b0cfc0c300 100644 --- a/cmake/python_scanners.cmake +++ b/cmake/python_scanners.cmake @@ -53,6 +53,14 @@ macro(check_python_scanner_modules name modules packages) if(packages_missing_${name}) string (REPLACE "," " " missing "${packages_missing_${name}}") message(" To enable the scanner ${name}, please install the following Python packages:${missing}") + # Record the missing-package status for the list-scanners helper. Strip the + # leading comma left by the accumulator and re-encode "," as "+" to keep + # the per-scanner record self-contained inside our ";"-separated list. + string(REGEX REPLACE "^," "" _missing_csv "${packages_missing_${name}}") + string(REPLACE "," "+" _missing_csv "${_missing_csv}") + list(APPEND GAMBIT_PYTHON_SCANNER_STATUS "${name}|missing|${_missing_csv}") + else() + list(APPEND GAMBIT_PYTHON_SCANNER_STATUS "${name}|enabled|") endif() endmacro() diff --git a/cmake/scripts/update_cmakelists.py b/cmake/scripts/update_cmakelists.py index 36d2575b50..01aeb07858 100644 --- a/cmake/scripts/update_cmakelists.py +++ b/cmake/scripts/update_cmakelists.py @@ -55,16 +55,23 @@ def main(argv): # List of backends to exclude; subdirectories within the Backends/frontends directories # that match these strings will be ignored. exclude_backends=set([]) # -Ditch'ed backends + enabled_bits=set([]) + force_keep_backends=set([]) # Handle command line options verbose = False + verbose_build = False try: - opts, args = getopt.getopt(argv,"vx:",["verbose","exclude-modules="]) + opts, args = getopt.getopt(argv,"vx:", + ["verbose","exclude-modules=","bits=","force-backends=","verbose-build"]) except getopt.GetoptError: print('Usage: update_cmakelists.py [flags]') print(' flags:') print(' -v : More verbose output') print(' -x module1,backendA,printer2,modelX,... : Exclude module1, backendA, printer2, modelX, etc.') + print(' --bits=B1,B2,... : Restrict the build to dependencies of these Bits.') + print(' --force-backends=A,B,... : Keep these backends, even if no enabled Bit needs them.') + print(' --verbose-build : Print which backend frontends were auto-excluded.') sys.exit(2) for opt, arg in opts: if opt in ('-v','--verbose'): @@ -74,6 +81,22 @@ def main(argv): exclude_modules.update(neatsplit(",",arg)) exclude_printers.update(neatsplit(",",arg)) exclude_backends.update(neatsplit(",",arg)) + elif opt == '--bits': + enabled_bits.update(b for b in neatsplit(",",arg) if b) + elif opt == '--force-backends': + force_keep_backends.update(b for b in neatsplit(",",arg) if b) + elif opt == '--verbose-build': + verbose_build = True + + # If --bits is given, drop backend frontends that no enabled Bit references. + if enabled_bits: + auto_excluded, _used, _all = derive_optin_backend_excludes( + enabled_bits, ".", + "./Backends/include/gambit/Backends/frontends", + force_keep_backends) + exclude_backends.update(auto_excluded) + if verbose_build or verbose: + print("update_cmakelists.py: auto-excluded {0} backend(s) from Backends/CMakeLists.txt.".format(len(auto_excluded))) # Find all the modules. modules = module_census(verbose,".",exclude_modules) diff --git a/cmake/utilities.cmake b/cmake/utilities.cmake index 5fbfdbc6e8..93c9c79b9e 100644 --- a/cmake/utilities.cmake +++ b/cmake/utilities.cmake @@ -265,14 +265,16 @@ macro(strip_library KEY LIBRARIES) set(FOUND_KEY2 "") endmacro() -# Function to add a GAMBIT custom command and target +# Function to add a GAMBIT custom command and target. +# A caller may set ${HARVESTER}_EXTRA_ARGS before invoking this +# macro to pass additional command-line flags to the harvester. macro(add_gambit_custom target filename HARVESTER DEPS) set(ditch_string "") if (NOT "${ARGN}" STREQUAL "") set(ditch_string "-x __not_a_real_name__,${ARGN}") endif() add_custom_command(OUTPUT ${CMAKE_BINARY_DIR}/${filename} - COMMAND ${Python3_EXECUTABLE} ${${HARVESTER}} ${ditch_string} + COMMAND ${Python3_EXECUTABLE} ${${HARVESTER}} ${ditch_string} ${${HARVESTER}_EXTRA_ARGS} COMMAND touch ${CMAKE_BINARY_DIR}/${filename} WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} DEPENDS ${${HARVESTER}} @@ -741,3 +743,215 @@ endmacro() macro(BOSS_backend name backend_version ${ARGN}) BOSS_backend_full(${name} ${backend_version} ${ARGN}) endmacro() + + +# Function to translate -DBits="Bit1;Bit2" into additions to ${itch}. +# Call before retrieve_bits; reads the cmake variable `Bits` and the +# already-populated ${ALL_GAMBIT_BITS}. ScannerBit is implicitly kept; +# any explicit -Ditch entries also stand. +# Sets ${out_var} to YES if ${token} matches any of ${ARGN} (case-insensitive). +function(_gambit_list_contains_ci out_var token) + string(TOLOWER "${token}" _t_lower) + set(${out_var} NO PARENT_SCOPE) + foreach(_x ${ARGN}) + string(TOLOWER "${_x}" _x_lower) + if(_x_lower STREQUAL _t_lower) + set(${out_var} YES PARENT_SCOPE) + return() + endif() + endforeach() +endfunction() + + +function(gambit_translate_bits_into_itch) + if(NOT DEFINED Bits OR "${Bits}" STREQUAL "") + return() + endif() + + # A Bit can't be both kept (-DBits) and ditched (-Ditch). Error rather + # than silently letting -Ditch win, which would otherwise quietly drop a + # Bit the user explicitly asked to keep. + set(_conflicts "") + foreach(_b ${Bits}) + _gambit_list_contains_ci(_in_itch "${_b}" ${itch}) + if(_in_itch) + list(APPEND _conflicts "${_b}") + endif() + endforeach() + if(_conflicts) + list(REMOVE_DUPLICATES _conflicts) + string(REPLACE ";" ", " _conflicts_csv "${_conflicts}") + message(FATAL_ERROR + "\nThe following entries appear in both -DBits and -Ditch: " + "${_conflicts_csv}.\n" + "A Bit cannot be simultaneously kept and ditched. Please remove " + "the conflicting entries from one of the two arguments and " + "reconfigure.\n") + endif() + + message("${BoldYellow}-- Bits=\"${Bits}\" requested; restricting build to these Bits (+ ScannerBit).${ColourReset}") + + # Warn about -DBits entries that don't match a real Bit directory. + set(_unknown_Bits "") + foreach(_b ${Bits}) + _gambit_list_contains_ci(_match_found "${_b}" ${ALL_GAMBIT_BITS}) + if(NOT _match_found) + list(APPEND _unknown_Bits "${_b}") + endif() + endforeach() + if(_unknown_Bits) + message("${BoldRed} -DBits entries with no matching Bit directory (ignored): ${_unknown_Bits}${ColourReset}") + endif() + + # Add every non-listed (and non-ScannerBit) Bit to ${itch}. + set(_added_to_itch "") + set(_local_itch "${itch}") + foreach(_d ${ALL_GAMBIT_BITS}) + string(TOLOWER "${_d}" _d_lower) + if(NOT _d_lower STREQUAL "scannerbit") + _gambit_list_contains_ci(_keep "${_d}" ${Bits}) + if(NOT _keep) + list(APPEND _added_to_itch ${_d}) + set(_local_itch "${_local_itch};${_d}") + endif() + endif() + endforeach() + if(_added_to_itch) + message("${Yellow} Bits added to ditch list by -DBits: ${_added_to_itch}${ColourReset}") + endif() + set(itch "${_local_itch}" PARENT_SCOPE) +endfunction() + + +# Function to default GAMBIT_TRIM_BACKEND_INTERFACES to ON if any on-disk Bit +# was excluded, OFF otherwise. A user-specified value (ON or OFF) takes +# precedence. +function(_gambit_resolve_trim_default) + if(DEFINED GAMBIT_TRIM_BACKEND_INTERFACES) + return() + endif() + set(_user_narrowed FALSE) + foreach(_d ${ALL_GAMBIT_BITS}) + if(NOT (";${GAMBIT_BITS};" MATCHES ";${_d};")) + set(_user_narrowed TRUE) + endif() + endforeach() + if(_user_narrowed) + set(GAMBIT_TRIM_BACKEND_INTERFACES ON CACHE BOOL + "Skip backend interfaces that no enabled Bit references." FORCE) + message("${BoldYellow}-- One or more Bits were excluded; enabling GAMBIT_TRIM_BACKEND_INTERFACES=ON.${ColourReset}") + else() + set(GAMBIT_TRIM_BACKEND_INTERFACES OFF CACHE BOOL + "Skip backend interfaces that no enabled Bit references.") + endif() +endfunction() + + +# Function to print a configure-time summary of which backend interfaces are +# active and which are disabled, and to return the list of disabled backend +# names in ${out_disabled_canonical} (canonical BACKENDNAMEs). +function(_gambit_print_optin_summary bits_with_commas force_with_commas out_disabled_canonical) + execute_process( + COMMAND ${Python3_EXECUTABLE} -c " +import sys +sys.path.insert(0, 'Utils/scripts') +from harvesting_tools import derive_optin_backend_excludes +bits = [b for b in '${bits_with_commas}'.split(',') if b] +force = [b for b in '${force_with_commas}'.split(',') if b] +auto_excluded, used, all_be = derive_optin_backend_excludes( + bits, '.', './Backends/include/gambit/Backends/frontends', force) +unknown_force = [f for f in force if f not in all_be] +print('USED:' + ','.join(sorted(used))) +print('SKIPPED:' + ','.join(sorted(auto_excluded))) +print('UNKNOWN_FORCE:' + ','.join(unknown_force)) +print('TOTAL:{0}'.format(len(all_be))) +" + OUTPUT_VARIABLE _trim_summary + RESULT_VARIABLE _trim_rc + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}) + if(NOT _trim_rc EQUAL 0) + set(${out_disabled_canonical} "" PARENT_SCOPE) + return() + endif() + + string(REGEX MATCH "USED:[^\n]*" _used_line "${_trim_summary}") + string(REGEX MATCH "SKIPPED:[^\n]*" _skipped_line "${_trim_summary}") + string(REGEX MATCH "UNKNOWN_FORCE:[^\n]*" _unknown_line "${_trim_summary}") + string(REGEX MATCH "TOTAL:[0-9]+" _total_line "${_trim_summary}") + string(REPLACE "USED:" "" _used_list "${_used_line}") + string(REPLACE "SKIPPED:" "" _skipped_list "${_skipped_line}") + string(REPLACE "UNKNOWN_FORCE:" "" _unknown_force "${_unknown_line}") + string(REPLACE "TOTAL:" "" _total_count "${_total_line}") + string(REPLACE "," ";" _used_list_sc "${_used_list}") + string(REPLACE "," ";" _skipped_list_sc "${_skipped_list}") + list(LENGTH _used_list_sc _used_count) + list(LENGTH _skipped_list_sc _skipped_count) + if("${_used_list}" STREQUAL "") + set(_used_count 0) + set(_used_list_sc "") + endif() + if("${_skipped_list}" STREQUAL "") + set(_skipped_count 0) + set(_skipped_list_sc "") + endif() + + message("${BoldYellow}-- GAMBIT_TRIM_BACKEND_INTERFACES is ON. Building ${_used_count} of ${_total_count} backend interfaces.${ColourReset}") + if(NOT "${_unknown_force}" STREQUAL "") + message("${BoldRed} -DGAMBIT_FORCE_BACKEND_INTERFACE entries with no matching backend (ignored): ${_unknown_force}${ColourReset}") + endif() + + # Print every backend; active in white, disabled in yellow with a [disabled] tag. + set(_all_backends ${_used_list_sc} ${_skipped_list_sc}) + list(SORT _all_backends) + foreach(_be ${_all_backends}) + if(NOT "${_be}" STREQUAL "") + list(FIND _skipped_list_sc "${_be}" _is_skipped) + if(_is_skipped EQUAL -1) + message("${BoldWhite} ${_be}${ColourReset}") + else() + message("${Yellow} ${_be} [disabled]${ColourReset}") + endif() + endif() + endforeach() + if(_skipped_count GREATER 0) + message("${Yellow} Pass -DGAMBIT_FORCE_BACKEND_INTERFACE=\"Name1;Name2\" to keep one anyway,${ColourReset}") + message("${Yellow} or -DGAMBIT_TRIM_BACKEND_INTERFACES=OFF to disable trimming.${ColourReset}") + endif() + + set(${out_disabled_canonical} "${_skipped_list_sc}" PARENT_SCOPE) +endfunction() + + +# Function to resolve the opt-in build configuration after retrieve_bits has +# set ${GAMBIT_BITS}. Sets BACKEND_HARVESTER_EXTRA_ARGS and +# update_cmakelists_extra_args in the caller's scope, and (when trimming is +# enabled) appends the disabled backend names to ${itch} so that the existing +# check_ditch_status mechanism in cmake/externals.cmake also disables the +# corresponding download/install targets. +function(gambit_configure_optin_build) + _gambit_resolve_trim_default() + + set(_extra_args "") + if(GAMBIT_TRIM_BACKEND_INTERFACES) + string(REPLACE ";" "," _bits_with_commas "${GAMBIT_BITS}") + list(APPEND _extra_args --bits=${_bits_with_commas}) + + set(_force_with_commas "") + if(DEFINED GAMBIT_FORCE_BACKEND_INTERFACE AND NOT "${GAMBIT_FORCE_BACKEND_INTERFACE}" STREQUAL "") + string(REPLACE ";" "," _force_with_commas "${GAMBIT_FORCE_BACKEND_INTERFACE}") + list(APPEND _extra_args --force-backends=${_force_with_commas}) + endif() + if(GAMBIT_VERBOSE_BUILD) + list(APPEND _extra_args --verbose-build) + endif() + + _gambit_print_optin_summary("${_bits_with_commas}" "${_force_with_commas}" _disabled_backends) + foreach(_be ${_disabled_backends}) + list(APPEND itch "${_be}") + endforeach() + set(itch "${itch}" PARENT_SCOPE) + endif() + + set(BACKEND_HARVESTER_EXTRA_ARGS "${_extra_args}" PARENT_SCOPE) + set(update_cmakelists_extra_args "${_extra_args}" PARENT_SCOPE) +endfunction()