diff --git a/.gitignore b/.gitignore
index 521dcc53cced8219c7d446af100c882601af237f..5b85a81865966bf791ed8dc91d533bb33db4be4a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -189,15 +189,6 @@ Makefile.plugin.generated
 # WP/Coq Generated file
 .lia.cache
 
-# analysis-scripts
-share/analysis-scripts/build
-share/analysis-scripts/fced-dist
-share/analysis-scripts/fced-dist-prepare*
-share/analysis-scripts/fc-estimate-difficulty.*spec
-share/analysis-scripts/fc-estimate-difficulty
-share/analysis-scripts/fc-estimate-difficulty.exe
-share/analysis-scripts/libc_metrics.json
-
 # generated ML files
 
 /src/libraries/utils/json.ml
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8968f1d721d01296a2463213c85d7d5dce6b73d3..9c8fa8d5ef8b62aab214cc09258852fa0a200f40 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -7,7 +7,7 @@ stages:
 
 variables:
     CURRENT: $CI_COMMIT_REF_NAME
-    DEFAULT: "master"
+    DEFAULT: "stable/manganese"
     FRAMA_CI_OPT: "--override frama-c:$CI_COMMIT_REF_NAME,$CI_COMMIT_SHA"
     OCAML: "4_08"
 
diff --git a/Makefile b/Makefile
index 0401f30e2cd4bed63bfc9449a1edd2c6cef4fd18..6b917cf38ef061f06e297d00a414b01557f93c12 100644
--- a/Makefile
+++ b/Makefile
@@ -265,9 +265,7 @@ DISTRIB_FILES:=\
       share/analysis-scripts/detect_recursion.py                        \
       share/analysis-scripts/epilogue.mk                                \
       share/analysis-scripts/estimate_difficulty.py                     \
-      share/analysis-scripts/external_tool.py                           \
       share/analysis-scripts/fc_stubs.c                                 \
-      share/analysis-scripts/fclog.py                                   \
       share/analysis-scripts/find_fun.py                                \
       share/analysis-scripts/flamegraph.pl                              \
       share/analysis-scripts/frama_c_results.py                         \
@@ -1984,9 +1982,7 @@ install:: install-lib-$(OCAMLBEST)
 	  share/analysis-scripts/clone.sh \
 	  share/analysis-scripts/creduce.sh \
 	  share/analysis-scripts/epilogue.mk \
-	  share/analysis-scripts/external_tool.py \
 	  share/analysis-scripts/fc_stubs.c \
-	  share/analysis-scripts/fclog.py \
 	  share/analysis-scripts/find_fun.py \
 	  share/analysis-scripts/flamegraph.pl \
 	  share/analysis-scripts/frama_c_results.py \
diff --git a/headers/header_spec.txt b/headers/header_spec.txt
index 39f3f30674713e60820f7f3b03b3003e9a6736ed..e34347acdd061e025cfff74a0507b0f5dfac4d46 100644
--- a/headers/header_spec.txt
+++ b/headers/header_spec.txt
@@ -123,9 +123,7 @@ share/analysis-scripts/creduce.sh: CEA_LGPL
 share/analysis-scripts/detect_recursion.py: CEA_LGPL
 share/analysis-scripts/epilogue.mk: CEA_LGPL
 share/analysis-scripts/estimate_difficulty.py: CEA_LGPL
-share/analysis-scripts/external_tool.py: CEA_LGPL
 share/analysis-scripts/fc_stubs.c: .ignore
-share/analysis-scripts/fclog.py: CEA_LGPL
 share/analysis-scripts/frama_c_results.py: CEA_LGPL
 share/analysis-scripts/cmd-dep.sh: .ignore
 share/analysis-scripts/concat-csv.sh: .ignore
diff --git a/share/analysis-scripts/build.py b/share/analysis-scripts/build.py
index c4982e88e0743a5f0ec998b3be688b87b75df4db..854fb82d011f36cb5fa593fee0b5e3a5a78e64d9 100755
--- a/share/analysis-scripts/build.py
+++ b/share/analysis-scripts/build.py
@@ -34,7 +34,6 @@ import re
 import shutil
 import sys
 import subprocess
-import typing
 
 import function_finder
 import source_filter
@@ -48,13 +47,6 @@ parser = argparse.ArgumentParser(
 for analysis with Frama-C. Tries to use a build_commands.json file if
 available."""
 )
-parser.add_argument(
-    "--base",
-    metavar="DIR",
-    default=".",
-    help="base directory used for pretty-printing relative paths (default: PWD)",
-    type=Path,
-)
 parser.add_argument(
     "--debug",
     metavar="FILE",
@@ -66,7 +58,6 @@ parser.add_argument(
     metavar="FILE",
     default="build_commands.json",
     help="path to JBDB (default: build_commands.json)",
-    type=Path,
 )
 parser.add_argument(
     "--machdep", metavar="MACHDEP", help="analysis machdep (default: Frama-C's default)"
@@ -77,11 +68,6 @@ parser.add_argument(
     default="main",
     help="name of the main function (default: main)",
 )
-parser.add_argument(
-    "--no-source-filter",
-    action="store_true",
-    help="disable source filters (less precise, but speeds up large projects)",
-)
 parser.add_argument(
     "--sources",
     metavar="FILE",
@@ -93,17 +79,15 @@ parser.add_argument(
     "--targets",
     metavar="FILE",
     nargs="+",
-    help="targets to build. When using --sources, only a single target is allowed.",
+    help="targets to build. When using --sources, " + "only a single target is allowed.",
     type=Path,
 )
 
 args = parser.parse_args()
-base = args.base
 force = args.force
 jbdb_path = args.jbdb
 machdep = args.machdep
 main = args.main
-no_source_filter = args.no_source_filter
 sources = args.sources
 targets = args.targets
 debug = args.debug
@@ -128,20 +112,21 @@ dot_framac_dir = Path(".frama-c")
 
 # Check required environment variables and commands in the PATH ###############
 
-framac_bin = Path(
-    os.getenv("FRAMAC_BIN")
-    or sys.exit("error: FRAMAC_BIN not in environment (set by frama-c-script)")
-)
+framac_bin = os.getenv("FRAMAC_BIN")
+if not framac_bin:
+    sys.exit("error: FRAMAC_BIN not in environment (set by frama-c-script)")
+framac_bin = Path(framac_bin)
 
 under_test = os.getenv("PTESTS_TESTING")
 
 # Prepare blug-related variables and functions ################################
 
-blug = Path(
-    os.getenv("BLUG")
-    or shutil.which("blug")
-    or sys.exit("error: path to 'blug' binary must be in PATH or variable BLUG")
-)
+blug = os.getenv("BLUG")
+if not blug:
+    blug = shutil.which("blug")
+    if not blug:
+        sys.exit("error: path to 'blug' binary must be in PATH or variable BLUG")
+blug = Path(blug)
 blug_dir = blug.resolve().parent
 # to import blug_jbdb
 sys.path.insert(0, blug_dir.as_posix())
@@ -204,18 +189,14 @@ def make_target_name(target):
     return prettify(target).replace("/", "_").replace(".", "_")
 
 
-def rel_path(path: Path, base: Path) -> str:
-    """Return a relative path to the .frama-c directory, if path is relative to base.
-    Otherwise, return an absolute path. Typically, base is the parent of the .frama-c directory."""
-    try:
-        path.resolve().relative_to(base.resolve())  # Fails if path is not inside base
-        return os.path.relpath(path, start=dot_framac_dir)
-    except ValueError:
-        return str(path.resolve())
+# sources are pretty-printed relatively to the .frama-c directory, where the
+# GNUmakefile will reside
+def rel_prefix(path):
+    return path if os.path.isabs(path) else os.path.relpath(path, start=dot_framac_dir)
 
 
-def pretty_sources(sources, base):
-    return [f"  {rel_path(source, base)} \\" for source in sorted(sources)]
+def pretty_sources(sources):
+    return [f"  {rel_prefix(source)} \\" for source in sources]
 
 
 def lines_of_file(path):
@@ -248,7 +229,7 @@ def copy_fc_stubs():
 # [funcname] in [filename].
 # [has_args] is used to distinguish between main(void) and main(int, char**).
 def find_definitions(funcname, filename):
-    file_content = source_filter.open_and_filter(filename, not under_test and not no_source_filter)
+    file_content = source_filter.open_and_filter(filename, not under_test)
     file_lines = file_content.splitlines(keepends=True)
     newlines = function_finder.compute_newline_offsets(file_lines)
     defs = function_finder.find_definitions_and_declarations(
@@ -274,7 +255,7 @@ def list_partition(f, l):
     l1 = []
     l2 = []
     for e in l:
-        if f(e):
+        if f(l):
             l1.append(e)
         else:
             l2.append(e)
@@ -359,7 +340,7 @@ if unknown_sources:
 # We also need to check if the main function uses a 'main(void)'-style
 # signature, to patch fc_stubs.c.
 
-main_definitions: dict[Path, list[typing.Tuple[Path, str, str]]] = {}
+main_definitions = {}
 for target, sources in sources_map.items():
     main_definitions[target] = []
     for source in sources:
@@ -424,18 +405,18 @@ if jbdb_path:
     insert_lines_after(
         template,
         "^FCFLAGS",
-        [f"  -json-compilation-database {rel_path(jbdb_path, base)} \\"],
+        [f"  -json-compilation-database {rel_prefix(jbdb_path)} \\"],
     )
 
-targets_eva = [f"  {make_target_name(target)}.eva \\" for target in sorted(targets)]
+targets_eva = [f"  {make_target_name(target)}.eva \\" for target in targets]
 replace_line(template, "^TARGETS = main.eva", "TARGETS = \\")
 insert_lines_after(template, r"^TARGETS = \\", targets_eva)
 
 delete_line(template, r"^main.parse: \\")
 delete_line(template, r"^  main.c \\")
-for target, sources in reversed(sorted(sources_map.items())):
+for target, sources in reversed(sources_map.items()):
     pp_target = make_target_name(target)
-    new_lines = [f"{pp_target}.parse: \\"] + pretty_sources(sources, base) + [""]
+    new_lines = [f"{pp_target}.parse: \\"] + pretty_sources(sources) + [""]
     if any(d[2] for d in main_definitions[target]):
         logging.debug(
             "target %s has main with args, adding -main eva_main to its FCFLAGS",
diff --git a/share/analysis-scripts/build_callgraph.py b/share/analysis-scripts/build_callgraph.py
index f9a6d60a4ddea7bda496a9e74b60a73a567b0ad1..8edc32655e804bacfbb7dd8ca1ccdc1faee21bc0 100755
--- a/share/analysis-scripts/build_callgraph.py
+++ b/share/analysis-scripts/build_callgraph.py
@@ -26,7 +26,6 @@ for a given function name, via heuristic syntactic matching."""
 
 import os
 import sys
-import typing
 
 import function_finder
 import source_filter
@@ -45,10 +44,10 @@ class Callgraph:
     """
 
     # maps each caller to the list of its callees
-    succs: dict[str, list[str]] = {}
+    succs = {}
 
     # maps (caller, callee) to the list of call locations
-    edges: dict[typing.Tuple[str, str], list[typing.Tuple[str, int]]] = {}
+    edges = {}
 
     def add_edge(self, caller, callee, loc):
         if (caller, callee) in self.edges:
diff --git a/share/analysis-scripts/estimate_difficulty.py b/share/analysis-scripts/estimate_difficulty.py
index c4f3b7833da1747ea08232b4c47f43efd2291f79..c8dcf2155da073f69ca173d02f251eaf3e19223b 100755
--- a/share/analysis-scripts/estimate_difficulty.py
+++ b/share/analysis-scripts/estimate_difficulty.py
@@ -26,23 +26,18 @@
 of analyzing a new code base with Frama-C."""
 
 import argparse
-import glob
 import json
-import logging
 import os
 from pathlib import Path
 import re
 import subprocess
-import sys
 import tempfile
-import typing
 
 import build_callgraph
-import external_tool
-import fclog
 import source_filter
 
 # TODO : avoid relativizing paths when introducing too many ".." ;
+# TODO : accept directory as argument (--full-tree), and then do glob **/*.{c,i} inside
 # TODO : try to check the presence of compiler builtins
 # TODO : try to check for pragmas
 # TODO : detect absence of 'main' function (library)
@@ -52,75 +47,34 @@ parser = argparse.ArgumentParser(
 Estimates the difficulty of analyzing a given code base"""
 )
 parser.add_argument(
-    "paths",
+    "--header-dirs",
+    "-d",
+    metavar="DIR",
     nargs="+",
-    help="source files and directories. \
-If a directory <dir> is specified, it is recursively explored, as if '<dir>/**/*.[ci]' \
-had been specified.",
-    type=Path,
+    help="directories containing headers (default: .frama-c)",
 )
-parser.add_argument(
-    "--debug",
-    metavar="FILE",
-    help="enable debug mode and redirect output to the specified file",
-)
-parser.add_argument(
-    "--verbose",
-    action="store_true",
-    help="enable verbose output; if --debug is set, output is redirected to the same file.",
-)
-parser.add_argument(
-    "--no-cloc",
-    action="store_true",
-    help="disable usage of external tool 'cloc', even if available.",
-)
-args = parser.parse_args()
-paths = args.paths
-debug = args.debug
-no_cloc = args.no_cloc
-verbose = args.verbose
+parser.add_argument("files", nargs="+", help="source files")
+args = vars(parser.parse_args())
 
-under_test = os.getenv("PTESTS_TESTING")
-
-fclog.init(debug, verbose)
-
-### Auxiliary functions #######################################################
+header_dirs = args["header_dirs"]
+if not header_dirs:
+    header_dirs = []
+files = args["files"]
 
+under_test = os.getenv("PTESTS_TESTING")
 
-def collect_files_and_local_dirs(
-    paths: typing.Iterable[Path],
-) -> typing.Tuple[set[Path], set[Path]]:
-    """Returns the list of directories (and their subdirectories) containing
-    the specified paths. Note that this also includes subdirectories which do not
-    themselves contain any .c files, but which may contain .h files."""
-    dirs: set[Path] = set()
-    files: set[Path] = set()
-    for p in paths:
-        if p.is_dir():
-            files = files.union([Path(p) for p in glob.glob(f"{p}/**/*.[chi]", recursive=True)])
-            dirs.add(p)
-        else:
-            files.add(p)
-            dirs.add(p.parent)
-    local_dirs = {Path(s[0]) for d in dirs for s in os.walk(d)}
-    if not files:
-        sys.exit(
-            "error: no source files (.c/.i) found in provided paths: "
-            + " ".join([str(p) for p in paths])
-        )
-    return files, local_dirs
+# gather information from several sources
 
 
 def extract_keys(l):
     return [list(key.keys())[0] for key in l]
 
 
-def get_framac_libc_function_statuses(
-    framac: typing.Optional[Path], framac_share: Path
-) -> typing.Tuple[list[str], list[str]]:
+def get_framac_libc_function_statuses(framac, framac_share):
     if framac:
         (_handler, metrics_tmpfile) = tempfile.mkstemp(prefix="fc_script_est_diff", suffix=".json")
-        logging.debug("metrics_tmpfile: %s", metrics_tmpfile)
+        if debug:
+            print("metrics_tmpfile: %s", metrics_tmpfile)
         fc_runtime = framac_share / "libc" / "__fc_runtime.c"
         fc_libc_headers = framac_share / "libc" / "__fc_libc.h"
         subprocess.run(
@@ -150,8 +104,10 @@ def get_framac_libc_function_statuses(
     return (defined, spec_only)
 
 
-def grep_includes_in_file(filename: Path):
-    re_include = re.compile(r'\s*#\s*include\s*("|<)([^">]+)("|>)')
+re_include = re.compile(r'\s*#\s*include\s*("|<)([^">]+)("|>)')
+
+
+def grep_includes_in_file(filename):
     file_content = source_filter.open_and_filter(filename, not under_test)
     i = 0
     for line in file_content.splitlines():
@@ -163,9 +119,9 @@ def grep_includes_in_file(filename: Path):
             yield (i, kind, header)
 
 
-def get_includes(files: typing.Iterable[Path]):
-    quote_includes: dict[Path, list[typing.Tuple[Path, int]]] = {}
-    chevron_includes: dict[Path, list[typing.Tuple[Path, int]]] = {}
+def get_includes(files):
+    quote_includes = {}
+    chevron_includes = {}
     for filename in files:
         for line, kind, header in grep_includes_in_file(filename):
             if kind == "<":
@@ -180,56 +136,13 @@ def get_includes(files: typing.Iterable[Path]):
     return chevron_includes, quote_includes
 
 
-def is_local_header(local_dirs: typing.Iterable[Path], header: Path):
-    for d in local_dirs:
-        if (d / header).exists():
-            return True
-    return False
-
-
-def grep_keywords(keywords: list[str], filename: Path) -> dict[str, int]:
-    with open(filename, "r") as f:
-        found: dict[str, int] = {}
-        for line in f:
-            for kw in keywords:
-                if kw in line:
-                    if kw in found:
-                        found[kw] += 1
-                    else:
-                        found[kw] = 1
-        return found
-
-
-def pretty_keyword_count(found: dict[str, int]) -> str:
-    res = ""
-    for kw, count in sorted(found.items()):
-        if res:
-            res += ", "
-        res += f" {kw} ({count} line{'s' if count > 1 else ''})"
-    return res
-
-
-### End of auxiliary functions ################################################
-
 debug = os.getenv("DEBUG")
 verbose = False
 
-files, local_dirs = collect_files_and_local_dirs(paths)
-
-score = {
-    "recursion": 0,
-    "libc": 0,
-    "includes": 0,
-    "malloc": 0,
-    "keywords": 0,
-    "asm": 0,
-}
-
 framac_bin = os.getenv("FRAMAC_BIN")
 if not framac_bin:
-    logging.info(
-        "Running script in no-Frama-C mode (set FRAMAC_BIN to the directory"
-        + " containing frama-c if you want to use it)."
+    print(
+        "Running script in no-Frama-C mode (set FRAMAC_BIN to the directory containing frama-c if you want to use it)."
     )
     framac = None
     script_dir = os.path.dirname(os.path.realpath(__file__))
@@ -240,28 +153,10 @@ else:
         subprocess.check_output([framac, "-no-autoload-plugins", "-print-share-path"]).decode()
     )
 
-
-if not no_cloc:
-    cloc = external_tool.get_command("cloc", "CLOC")
-    if cloc:
-        data = external_tool.run_and_check(
-            [str(cloc), "--hide-rate", "--progress-rate=0", "--csv"] + list(str(f) for f in files),
-            "",
-        )
-        data = data.splitlines()
-        [nfiles, _sum, nblank, ncomment, ncode] = data[-1].split(",")
-        nlines = int(nblank) + int(ncomment) + int(ncode)
-        logging.info(
-            "Processing %d file(s), approx. %d lines of code (out of %d lines)",
-            int(nfiles),
-            int(ncode),
-            nlines,
-        )
-
-logging.info("Building callgraph...")
+print("Building callgraph...")
 cg = build_callgraph.compute(files)
 
-logging.info("Computing data about libc/POSIX functions...")
+print("Computing data about libc/POSIX functions...")
 libc_defined_functions, libc_specified_functions = get_framac_libc_function_statuses(
     framac, framac_share
 )
@@ -277,7 +172,7 @@ with open(framac_share / "compliance" / "posix_identifiers.json", encoding="utf-
     posix_identifiers = all_data["data"]
     posix_headers = all_data["headers"]
 
-recursive_cycles: list[typing.Tuple[typing.Tuple[str, int], list[typing.Tuple[str, str]]]] = []
+recursive_cycles = []
 reported_recursive_pairs = set()
 build_callgraph.compute_recursive_cycles(cg, recursive_cycles)
 for (cycle_start_loc, cycle) in recursive_cycles:
@@ -290,22 +185,16 @@ for (cycle_start_loc, cycle) in recursive_cycles:
         continue
     reported_recursive_pairs.add(cycle[-1])
     (filename, line) = cycle_start_loc
-
-    def pretty_cycle(cycle):
-        (x, y) = cycle[0]
-        res = f"{x} -> {y}"
-        for (x, y) in cycle[1:]:
-            res += f" -> {y}"
-        return res
-
-    logging.info(
-        "[recursion] found recursive cycle near %s:%d: %s", filename, line, pretty_cycle(cycle)
-    )
-    score["recursion"] += 1
-
-callees = set(callee for (_, callee) in list(cg.edges.keys()))
+    (x, y) = cycle[0]
+    pretty_cycle = f"{x} -> {y}"
+    for (x, y) in cycle[1:]:
+        pretty_cycle += f" -> {y}"
+    print(f"[recursion] found recursive cycle near {filename}:{line}: {pretty_cycle}")
+
+callees = [callee for (_, callee) in list(cg.edges.keys())]
+callees = set(callees)
 used_headers = set()
-logging.info("Estimating difficulty for %d function calls...", len(callees))
+print(f"Estimating difficulty for {len(callees)} function calls...")
 warnings = 0
 
 for callee in sorted(callees):
@@ -314,10 +203,8 @@ for callee in sorted(callees):
         global warnings
         if status == "warning":
             warnings += 1
-        if status == "warning":
-            logging.warning("%s (%s) %s", callee, standard, reason)
-        else:
-            logging.log(fclog.VERBOSE, "%s: %s (%s) %s", status, callee, standard, reason)
+        if verbose or debug or status == "warning":
+            print(f"- {status}: {callee} ({standard}) {reason}")
 
     try:
         is_problematic = posix_identifiers[callee]["notes"]["fc-support"] == "problematic"
@@ -371,56 +258,55 @@ for callee in sorted(callees):
             if not status_emitted:
                 callee_status("warning", standard, "has neither code nor spec in Frama-C's libc")
 
-logging.info("Function-related warnings: %d", warnings)
-score["libc"] = warnings
+print(f"Function-related warnings: {warnings}")
 
-logging.log(
-    fclog.VERBOSE,
-    "Used POSIX headers:\n%s",
-    "\n".join([f"  <{header}>" for header in sorted(used_headers)]),
-)
+if (verbose or debug) and used_headers:
+    print("used headers:")
+    for header in sorted(used_headers):
+        print(f"  <{header}>")
 
 (chevron_includes, quote_includes) = get_includes(files)
 
-logging.info(
-    "Estimating difficulty for %d '#include <header>' directives...", len(chevron_includes)
-)
+
+def is_local_header(header_dirs, header):
+    for d in header_dirs:
+        path = Path(d)
+        if Path(path / header).exists():
+            return True
+    return False
+
+
+print(f"Estimating difficulty for {len(chevron_includes)} '#include <header>' directives...")
 non_posix_headers = []
 header_warnings = 0
 for header in sorted(chevron_includes, key=str.casefold):
-    if not header.lower().endswith(".h"):
-        continue  # ignore included non-header files
     if header in posix_headers:
         fc_support = posix_headers[header]["fc-support"]
         if fc_support == "unsupported":
             header_warnings += 1
-            logging.warning("included header <%s> is explicitly unsupported by Frama-C", header)
+            print(f"- WARNING: included header <{header}> is explicitly unsupported by Frama-C")
         else:
-            logging.log(
-                fclog.VERBOSE,
-                "included %s header %s",
-                "C11" if header in c11_headers else "POSIX",
-                header,
-            )
+            if verbose or debug:
+                c11_or_posix = "C11" if header in c11_headers else "POSIX"
+                print(f"- note: included {c11_or_posix} header ")
     else:
-        if is_local_header(local_dirs, header):
-            logging.log(
-                fclog.VERBOSE, "ok: included header <%s> seems to be available locally", header
-            )
+        if is_local_header(header_dirs, header):
+            if verbose or debug:
+                print(f"- ok: included header <{header}> seems to be available locally")
         else:
             non_posix_headers.append(header)
             header_warnings += 1
-            logging.warning("included non-POSIX header <%s>", header)
-logging.info("Header-related warnings: %d", header_warnings)
-score["includes"] = header_warnings
+            print(f"- warning: included non-POSIX header <{header}>")
+print(f"Header-related warnings: {header_warnings}")
+
 
 # dynamic allocation
 
 dynalloc_functions = set(["malloc", "calloc", "free", "realloc", "alloca", "mmap"])
 dyncallees = dynalloc_functions.intersection(callees)
 if dyncallees:
-    logging.info("Calls to dynamic allocation functions: %s", ", ".join(sorted(dyncallees)))
-    score["malloc"] = len(dyncallees)
+    print(f"- note: calls to dynamic allocation functions: {', '.join(sorted(dyncallees))}")
+
 
 # unsupported C11-specific features
 
@@ -434,21 +320,21 @@ c11_unsupported = [
     "alignof",  # stdalign.h may use these symbols
 ]
 
-logging.info("Checking presence of unsupported C11 features...")
-
-for fi in files:
-    found = grep_keywords(c11_unsupported, fi)
-    if found:
-        logging.warning("unsupported keyword(s) in %s:%s", fi, pretty_keyword_count(found))
-    score["keywords"] += len(found)
+for keyword in c11_unsupported:
+    out = subprocess.Popen(
+        ["grep", "-n", "\\b" + keyword + "\\b"] + files + ["/dev/null"],
+        stdout=subprocess.PIPE,
+        stderr=subprocess.STDOUT,
+    )
+    lines = out.communicate()[0].decode("utf-8").splitlines()
+    if lines:
+        n = len(lines)
+        print(
+            f"- warning: found {n} line{'s' if n > 1 else ''} with occurrences of "
+            f"unsupported C11 construct '{keyword}'"
+        )
 
 # assembly code
 
 if "asm" in callees or "__asm" in callees or "__asm__" in callees:
-    logging.warning("code seems to contain inline assembly ('asm(...)')")
-    score["asm"] = 1
-
-logging.info(
-    "Overall difficulty score:\n%s",
-    "\n".join([k + ": " + str(v) for (k, v) in sorted(score.items())]),
-)
+    print("- warning: code seems to contain inline assembly ('asm(...)')")
diff --git a/share/analysis-scripts/external_tool.py b/share/analysis-scripts/external_tool.py
deleted file mode 100644
index b660423228dd89ce923e80d4988219aed179d9b9..0000000000000000000000000000000000000000
--- a/share/analysis-scripts/external_tool.py
+++ /dev/null
@@ -1,76 +0,0 @@
-# -*- coding: utf-8 -*-
-##########################################################################
-#                                                                        #
-#  This file is part of Frama-C.                                         #
-#                                                                        #
-#  Copyright (C) 2007-2022                                               #
-#    CEA (Commissariat à l'énergie atomique et aux énergies              #
-#         alternatives)                                                  #
-#                                                                        #
-#  you can redistribute it and/or modify it under the terms of the GNU   #
-#  Lesser General Public License as published by the Free Software       #
-#  Foundation, version 2.1.                                              #
-#                                                                        #
-#  It is distributed in the hope that it will be useful,                 #
-#  but WITHOUT ANY WARRANTY; without even the implied warranty of        #
-#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         #
-#  GNU Lesser General Public License for more details.                   #
-#                                                                        #
-#  See the GNU Lesser General Public License version 2.1                 #
-#  for more details (enclosed in the file licenses/LGPLv2.1).            #
-#                                                                        #
-##########################################################################
-
-"""This file provides utility functions to use external tools, either
-available in the PATH or via PyInstaller."""
-
-import os
-from pathlib import Path
-import shutil
-import subprocess
-import sys
-import typing
-
-# warnings about missing commands are disabled during testing
-emit_warns = os.getenv("PTESTS_TESTING") is None
-
-# Cache for get_command
-cached_commands: dict[str, typing.Union[Path, None]] = {}
-
-
-def resource_path(relative_path: str) -> str:
-    """Get absolute path to resource; only used by the pyinstaller standalone distribution"""
-    base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
-    return os.path.join(base_path, relative_path)
-
-
-def get_command(command: str, env_var_name: str) -> typing.Union[Path, None]:
-    """Returns a Path to the command; priority goes to the environment variable,
-    then in the PATH, then in the resource directory (for a pyinstaller binary)."""
-    if command in cached_commands:
-        return cached_commands[command]
-    p = Path(os.getenv(env_var_name) or shutil.which(command) or resource_path(command))
-    if p.exists():
-        cached_commands[command] = p
-        return p
-    else:
-        if emit_warns:
-            print(
-                f"info: optional external command '{command}' not found in PATH;",
-                f"consider installing it or setting environment variable {env_var_name}",
-            )
-        cached_commands[command] = None
-        return None
-
-
-def run_and_check(command_and_args: list[str], input_data: str):
-    try:
-        return subprocess.check_output(
-            command_and_args,
-            input=input_data,
-            stderr=None,
-            encoding="ascii",
-            errors="ignore",
-        )
-    except subprocess.CalledProcessError as e:
-        sys.exit(f"error running command: {command_and_args}\n{e}")
diff --git a/share/analysis-scripts/fc-estimate-difficulty.mk b/share/analysis-scripts/fc-estimate-difficulty.mk
deleted file mode 100644
index 8cfae714209e0fe9458c16346c021170883b272d..0000000000000000000000000000000000000000
--- a/share/analysis-scripts/fc-estimate-difficulty.mk
+++ /dev/null
@@ -1,142 +0,0 @@
-# Produces a self-contained binary for 'frama-c-script estimate-difficulty'
-# on a machine without Frama-C.
-# Works on Linux and macOS. The Windows binary is compiled and tested using
-# a Docker image with wine and pyinstaller (usage of Docker avoids issues with
-# staging versions in e.g. Fedora, which may cause weird error messages when
-# running in Windows). The Linux binary is tested on a bare-bones Alpine Linux,
-# to ensure it does not depend on dynamic libraries.
-# The macOS version needs to be tested by hand.
-
-help:
-	@echo "targets:"
-	@echo "fc-estimate-difficulty           : Linux/macOS version (depending on host system)"
-	@echo "fc-estimate-difficulty.exe       : Windows cross-compiled version (from Linux)"
-	@echo "clean                            : Erase working directory"
-	@echo "test-fc-estimate-difficulty      : Test Linux/macOS version"
-	@echo "test-fc-estimate-difficulty.exe  : Test Windows version"
-
-os := $(shell uname -s)
-
-workdir = fced-dist-prepare
-distdir = fced-dist
-
-libc_metrics.json: ../libc/__fc_libc.h ../libc/__fc_runtime.c
-ifeq ($(wildcard $(FRAMAC)),)
-	$(error FRAMAC must be set to the path of the 'frama-c' binary)
-endif
-	FRAMAC_SHARE="$(shell $(FRAMAC) -no-autoload-plugins -print-share-path)"
-	rm -f $@
-	$(FRAMAC) \
-	  $^ \
-	  -no-autoload-plugins -load-module metrics \
-	  -metrics -metrics-libc -metrics-output $@
-	chmod -w $@ # generated file: prevent accidental overwriting
-
-PY_DEPS := \
-  build_callgraph.py \
-  estimate_difficulty.py \
-  external_tool.py \
-  fclog.py \
-  function_finder.py \
-  source_filter.py \
-
-COMPLIANCE := $(wildcard ../compliance/*.json)
-TOOLS := $(workdir)/scc $(workdir)/astyle
-COMMON_DEPS := $(PY_DEPS) $(COMPLIANCE) libc_metrics.json
-
-fc-estimate-difficulty: EXE=
-fc-estimate-difficulty.exe: EXE=".exe"
-
-fc-estimate-difficulty: WINE=
-fc-estimate-difficulty.exe: WINE="/home/andr/git/wine/wine"
-
-fc-estimate-difficulty: PSEP=:
-fc-estimate-difficulty.exe: PSEP=;
-
-fc-estimate-difficulty: $(COMMON_DEPS) $(TOOLS)
-fc-estimate-difficulty.exe: \
-  $(COMMON_DEPS) $(addsuffix .exe,$(TOOLS)) fced-win.Dockerfile
-
-fc-estimate-difficulty:
-	pyinstaller estimate_difficulty.py \
-	  -F \
-	  -n $@ \
-	  --distpath $(distdir) \
-	  --noconfirm \
-	  --add-data "libc_metrics.json:share" \
-	  --add-data "../compliance/*.json:share/compliance" \
-	  --add-binary "$(workdir)/scc:." \
-	  --add-binary "$(workdir)/astyle:."
-ifeq ($(os),Linux)
-	@echo "Linux: running staticx"
-	staticx "$(distdir)/$@" $@
-else
-	@echo "NOT running staticx (macOS?)"
-	mv "$(distdir)/$@" $@
-endif
-
-fc-estimate-difficulty.exe:
-	mkdir -p $(workdir)/compliance
-	cp ../compliance/*.json $(workdir)/compliance
-	docker build . -f fced-win.Dockerfile -t fced-win
-	docker cp $$(docker create --rm fced-win):/fced/$@ $@
-
-$(workdir)/scc: $(workdir)/scc-snapshots-fc-1.1.0
-	make -C $< clean
-	make -C $< scc $(if $(findstring Linux,$(os)),CFLAGS="-static") -j
-	cp $(workdir)/scc-snapshots-fc-1.1.0/scc $@
-
-$(workdir)/scc.exe: $(workdir)/scc-snapshots-fc-1.1.0
-	make -C $< clean
-	make -C $< scc CC=x86_64-w64-mingw32-gcc -j
-	cp $(workdir)/scc-snapshots-fc-1.1.0/scc.exe $@
-
-$(workdir)/scc-snapshots-fc-1.1.0:
-	mkdir -p $(workdir)
-	wget 'https://github.com/Frama-C/scc-snapshots/archive/refs/tags/fc-1.1.0.tar.gz' -O $(workdir)/fc-1.1.0.tar.gz
-	cd $(workdir) && tar xvf fc-1.1.0.tar.gz
-	touch $@
-
-$(workdir)/astyle: $(workdir)/astyle_3.1
-	make -C $(workdir)/astyle_3.1/build/gcc clean
-	make -C $(workdir)/astyle_3.1/build/gcc \
-	  $(if $(findstring Linux,$(os)),LDFLAGS="-static" CFLAGS="-static -static-libgcc -static-libstdc++") -j
-	cp $(workdir)/astyle_3.1/build/gcc/bin/astyle $@
-
-$(workdir)/astyle.exe: $(workdir)/astyle_3.1
-	make -C $(workdir)/astyle_3.1/build/gcc clean
-	make -C $(workdir)/astyle_3.1/build/gcc LDFLAGS="-static" CFLAGS="-static -static-libgcc -static-libstdc++" CXX=x86_64-w64-mingw32-g++ -j
-	cp $(workdir)/astyle_3.1/build/gcc/bin/astyle.exe $@
-
-$(workdir)/astyle_3.1:
-	mkdir -p $(workdir)
-	wget 'https://downloads.sourceforge.net/project/astyle/astyle/astyle%203.1/astyle_3.1_linux.tar.gz' -O $(workdir)/astyle_3.1_linux.tar.gz
-	cd $(workdir) && tar xvf astyle_3.1_linux.tar.gz
-	rm -rf $(workdir)/astyle_3.1
-	mv $(workdir)/astyle $(workdir)/astyle_3.1
-	sed -i.bak 's/^CXX = g++/CXX ?= g++/' $(workdir)/astyle_3.1/build/gcc/Makefile
-	sed -i.bak 's/^CXX = g++/CXX ?= g++/' $(workdir)/astyle_3.1/build/gcc/Makefile
-	touch $@
-
-clean:
-	rm -rf $(workdir) $(distdir) fc-estimate-difficulty.spec fc-estimate-difficulty.exe.spec
-
-distclean: clean
-	rm -f fc-estimate-difficulty fc-estimate-difficulty.exe libc_metrics.json
-
-test-fc-estimate-difficulty: fc-estimate-difficulty
-ifeq ($(os),Linux)
-	docker build . -f fced-lin.Dockerfile -t fced-lin
-else
-	./$< fced-test | \
-	  grep -A9 "Overall difficulty score" | \
-	  grep -v 0 | \
-	  grep -q ': '
-endif
-	@echo "Done testing $^."
-
-test-fc-estimate-difficulty.exe: fc-estimate-difficulty.exe
-	@file $< | grep -q "MS Windows"
-	@echo "Done testing $< (actual testing done inside Docker image)"
-
-.PHONY: clean distclean test-fc-estimate-difficulty test-fc-estimate-difficulty.exe
diff --git a/share/analysis-scripts/fced-lin.Dockerfile b/share/analysis-scripts/fced-lin.Dockerfile
deleted file mode 100644
index 9bf2aeed8b47f5c490cd3cf2ed8560e13e839506..0000000000000000000000000000000000000000
--- a/share/analysis-scripts/fced-lin.Dockerfile
+++ /dev/null
@@ -1,8 +0,0 @@
-FROM alpine:3.15
-COPY fc-estimate-difficulty /
-RUN mkdir /fced-test
-COPY fced-test fced-test/
-RUN ./fc-estimate-difficulty fced-test | \
-    grep -A9 "Overall difficulty score" | \
-    grep -v 0 | \
-    grep -q ': '
diff --git a/share/analysis-scripts/fced-test/a.c b/share/analysis-scripts/fced-test/a.c
deleted file mode 100644
index 9e3b8f5980c5180127180e585cbdecaa2d7f1d90..0000000000000000000000000000000000000000
--- a/share/analysis-scripts/fced-test/a.c
+++ /dev/null
@@ -1,11 +0,0 @@
-#include <stdlib.h>
-#include <X>
-
-volatile int v;
-
-int main() {
-  asm("mov ax, ax");
-  if (v) return main();
-  malloc(42);
-  return 0;
-}
diff --git a/share/analysis-scripts/fced-test/a.h b/share/analysis-scripts/fced-test/a.h
deleted file mode 100644
index 5e1d66f91d23143601cf71bfc6bd47adff296ffa..0000000000000000000000000000000000000000
--- a/share/analysis-scripts/fced-test/a.h
+++ /dev/null
@@ -1 +0,0 @@
-#include <a.h>
diff --git a/share/analysis-scripts/fced-win.Dockerfile b/share/analysis-scripts/fced-win.Dockerfile
deleted file mode 100644
index 1ae0faf9d29a083de5abbb267facb0ac4df6f67e..0000000000000000000000000000000000000000
--- a/share/analysis-scripts/fced-win.Dockerfile
+++ /dev/null
@@ -1,21 +0,0 @@
-FROM tobix/pywine:3.10
-RUN mkdir /fced
-COPY *.py /fced/
-RUN mkdir /fced/dist-prepare
-COPY fced-dist-prepare /fced/dist-prepare/
-COPY fced-test /fced/fced-test/
-COPY libc_metrics.json /fced/
-WORKDIR /fced
-RUN wine pyinstaller estimate_difficulty.py \
-	  -F \
-	  -n fc-estimate-difficulty.exe \
-	  --distpath . \
-	  --noconfirm \
-	  --add-data "libc_metrics.json;share" \
-	  --add-data "dist-prepare/compliance/*.json;share/compliance" \
-	  --add-binary "dist-prepare/scc.exe;." \
-	  --add-binary "dist-prepare/astyle.exe;."
-RUN wine fc-estimate-difficulty.exe fced-test | \
-    grep -A9 "Overall difficulty score" | \
-    grep -v 0 | \
-    grep -q ': '
diff --git a/share/analysis-scripts/fclog.py b/share/analysis-scripts/fclog.py
deleted file mode 100644
index 9355972bdd4d5b11d30243655fe4b3136d157446..0000000000000000000000000000000000000000
--- a/share/analysis-scripts/fclog.py
+++ /dev/null
@@ -1,77 +0,0 @@
-# -*- coding: utf-8 -*-
-##########################################################################
-#                                                                        #
-#  This file is part of Frama-C.                                         #
-#                                                                        #
-#  Copyright (C) 2007-2022                                               #
-#    CEA (Commissariat à l'énergie atomique et aux énergies              #
-#         alternatives)                                                  #
-#                                                                        #
-#  you can redistribute it and/or modify it under the terms of the GNU   #
-#  Lesser General Public License as published by the Free Software       #
-#  Foundation, version 2.1.                                              #
-#                                                                        #
-#  It is distributed in the hope that it will be useful,                 #
-#  but WITHOUT ANY WARRANTY; without even the implied warranty of        #
-#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         #
-#  GNU Lesser General Public License for more details.                   #
-#                                                                        #
-#  See the GNU Lesser General Public License version 2.1                 #
-#  for more details (enclosed in the file licenses/LGPLv2.1).            #
-#                                                                        #
-##########################################################################
-
-"""Contains functions to perform logging-related configuration common to
-   several scripts."""
-
-import logging
-import sys
-
-# extra log level used in messages
-VERBOSE = 15
-
-
-def init(debug, verbose):
-    """Initializes the logging mechanism. 'debug' is the filename to redirect to
-    (or special values 'stdout'/'stderr'). If None, disable debug.
-    If 'verbose' is true, output VERBOSE messages. If both are set, the level
-    is 'debug'."""
-
-    logging.addLevelName(VERBOSE, "")
-    logging.addLevelName(logging.ERROR, "ERROR: ")
-    logging.addLevelName(logging.WARN, "WARNING: ")
-    logging.addLevelName(logging.INFO, "")
-    log_level = logging.DEBUG if debug else VERBOSE if verbose else logging.INFO
-    log_format = "%(levelname)s%(message)s"
-
-    # special values for debug filename
-    if debug == "stdout" or not debug:
-        logging.basicConfig(stream=sys.stdout, level=log_level, format=log_format)
-        # This very ugly hack seems necessary to avoid BrokenPipeErrors when the
-        # output of the calling script is truncated (via head, grep -q, etc).
-        # Note that, on Windows, there is an OSError with "22 Invalid argument"
-        # instead of a BrokenPipeError.
-        original_flush = sys.stdout.flush
-
-        def new_flush():
-            try:
-                original_flush()
-            except BrokenPipeError:
-                sys.stdout = None
-                sys.exit(0)
-            except OSError as e:
-                if sys.platform == "win32" and e.errno == 22:
-                    sys.stdout = None
-                    sys.exit(0)
-                raise e
-
-        sys.stdout.flush = new_flush
-    elif debug == "stderr":
-        logging.basicConfig(stream=sys.stderr, level=log_level, format=log_format)
-    else:
-        logging.basicConfig(
-            filename=debug,
-            level=log_level,
-            filemode="w",
-            format=log_format,
-        )
diff --git a/share/analysis-scripts/list_files.py b/share/analysis-scripts/list_files.py
index c147503889bcdaac5bdf3e14c94ae684edd75277..691ef6ac04e66ffe535defeb241a95ed2beb9183 100755
--- a/share/analysis-scripts/list_files.py
+++ b/share/analysis-scripts/list_files.py
@@ -42,7 +42,7 @@ if not arg.exists():
     sys.exit(f"error: file '{arg}' not found")
 
 # check if arg has a known extension
-def is_known_c_extension(ext: str) -> bool:
+def is_known_c_extension(ext):
     return ext in (".c", ".i", ".ci", ".h")
 
 
@@ -51,10 +51,11 @@ fcmake_pwd = Path(pwd) / ".frama-c"  # pwd as seen by the Frama-C makefile
 with open(arg) as data:
     jcdb = json.load(data)
 jcdb_dir = arg.parent
-includes: set[Path] = set()
-defines: set[Path] = set()
+includes = set()
+defines = set()
 files = set()  # set of pairs of (file, file_for_fcmake)
 for entry in jcdb:
+    arg_includes = []  # before normalization
     if not "file" in entry:
         # ignore entries without a filename
         continue
@@ -80,9 +81,9 @@ print("")
 
 files_defining_main = set()
 re_main = re.compile(r"(int|void)\s+main\s*\([^)]*\)\s*\{")
-for (f, file_for_fcmake) in files:
-    assert os.path.exists(f), "file does not exist: %s" % f
-    with open(f, "r") as content_file:
+for (filename, file_for_fcmake) in files:
+    assert os.path.exists(filename), "file does not exist: %s" % filename
+    with open(filename, "r") as content_file:
         content = content_file.read()
         res = re.search(re_main, content)
         if res is not None:
diff --git a/share/analysis-scripts/make_wrapper.py b/share/analysis-scripts/make_wrapper.py
index 6f353d8f67c26a8cba4af8d12c2467721e32a1e7..7025db38a3d861520ef5a8fca21fb4f43a6a3d25 100755
--- a/share/analysis-scripts/make_wrapper.py
+++ b/share/analysis-scripts/make_wrapper.py
@@ -31,8 +31,6 @@ import os
 import re
 import subprocess
 import sys
-import typing
-
 from functools import partial
 
 # Check if GNU make is available and has the minimal required version
@@ -80,14 +78,12 @@ framac_script = f"{framac_bin}/frama-c-script"
 output_lines = []
 cmd_list = [make_cmd, "-C", make_dir] + args
 with subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc:
-    if not proc.stdout:
-        sys.exit(f"error capturing stdout for cmd: {cmd_list}")
     while True:
-        line_bytes = proc.stdout.readline()
-        if line_bytes:
-            sys.stdout.buffer.write(line_bytes)
+        line = proc.stdout.readline()
+        if line:
+            sys.stdout.buffer.write(line)
             sys.stdout.flush()
-            output_lines.append(line_bytes.decode("utf-8"))
+            output_lines.append(line.decode("utf-8"))
         else:
             break
 
@@ -103,7 +99,7 @@ for line in lines:
     match = re_recursive_call_start.search(line)
     if match:
 
-        def action_msg():
+        def _action():
             print(
                 "Consider patching, stubbing or adding an ACSL "
                 + "specification to the recursive call, "
@@ -129,7 +125,7 @@ for line in lines:
                 # note: this ending line can also match re_missing_spec
                 tip = {
                     "message": "Found recursive call at:\n" + "".join(msg_lines),
-                    "action": action_msg,
+                    "action": _action,
                 }
                 tips.append(tip)
                 break
@@ -140,7 +136,7 @@ for line in lines:
     if match:
         fname = match.group(1)
 
-        def action_find_fun(fname):
+        def _action(fname):
             out = subprocess.Popen(
                 [framac_script, "find-fun", "-C", make_dir, fname],
                 stdout=subprocess.PIPE,
@@ -182,7 +178,7 @@ for line in lines:
             + fname
             + "\n"
             + "   Looking for files defining it...",
-            "action": partial(action_find_fun, fname),
+            "action": partial(_action, fname),
         }
         tips.append(tip)
 
@@ -197,6 +193,6 @@ if tips != []:
         if counter > 1:
             print("")
             print("*** recommendation #" + str(counter) + " ***")
-        print(str(counter) + ". " + typing.cast(str, tip["message"]))
+        print(str(counter) + ". " + tip["message"])
         counter += 1
-        typing.cast(typing.Callable, tip["action"])()
+        tip["action"]()
diff --git a/share/analysis-scripts/normalize_jcdb.py b/share/analysis-scripts/normalize_jcdb.py
index 4b221b65f56c94305ab348605bdcfc86dc00fae4..64bf31d6aed119023c217a821387c792b8e26c52 100755
--- a/share/analysis-scripts/normalize_jcdb.py
+++ b/share/analysis-scripts/normalize_jcdb.py
@@ -44,6 +44,7 @@ if not arg.exists():
 with open(arg) as data:
     jcdb_json = json.load(data)
 jcdb_dir = arg.parent
+out_json = {}
 
 replacements = set()
 
diff --git a/share/analysis-scripts/source_filter.py b/share/analysis-scripts/source_filter.py
index 752cef5b07b43ef011b4ab3ba4bbd657f70d70c9..c255e89c28a0906f14964152f4fbb18ad914db00 100644
--- a/share/analysis-scripts/source_filter.py
+++ b/share/analysis-scripts/source_filter.py
@@ -37,33 +37,82 @@ the efficiency of regex-based heuristics."""
 # of errors when running the filters. Note that an absent tool
 # does _not_ lead to an error.
 
+import os
 from pathlib import Path
+import shutil
+import subprocess
 import sys
 
-import external_tool
+# warnings about missing commands are disabled during testing
+emit_warns = os.getenv("PTESTS_TESTING") is None
 
+# Cache for get_command
+cached_commands = {}
 
-def filter_with_scc(input_data: str) -> str:
-    scc_bin = "scc" if sys.platform != "win32" else "scc.exe"
-    scc = external_tool.get_command(scc_bin, "SCC")
+
+def resource_path(relative_path):
+    """Get absolute path to resource; only used by the pyinstaller standalone distribution"""
+    base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
+    return os.path.join(base_path, relative_path)
+
+
+def get_command(command, env_var_name):
+    """Returns a Path to the command; priority goes to the environment variable,
+    then in the PATH, then in the resource directory (for a pyinstaller binary)."""
+    if command in cached_commands:
+        return cached_commands[command]
+    p = os.getenv(env_var_name)
+    if p:
+        p = Path(p)
+    else:
+        p = shutil.which(command)
+        if p:
+            p = Path(p)
+        else:
+            p = Path(resource_path(command))
+            if not p.exists():
+                if emit_warns:
+                    print(
+                        f"info: optional external command '{command}' not found in PATH; "
+                        f"consider installing it or setting environment variable {env_var_name}"
+                    )
+                p = None
+    cached_commands[command] = p
+    return p
+
+
+def run_and_check(command_and_args, input_data):
+    try:
+        return subprocess.check_output(
+            command_and_args,
+            input=input_data,
+            stderr=None,
+            encoding="ascii",
+            errors="ignore",
+        )
+    except subprocess.CalledProcessError as e:
+        sys.exit(f"error running command: {command_and_args}\n{e}")
+
+
+def filter_with_scc(input_data):
+    scc = get_command("scc", "SCC")
     if scc:
-        return external_tool.run_and_check([str(scc), "-k", "-b"], input_data)
+        return run_and_check([scc, "-k"], input_data)
     else:
         return input_data
 
 
-def filter_with_astyle(input_data: str) -> str:
-    astyle_bin = "astyle" if sys.platform != "win32" else "astyle.exe"
-    astyle = external_tool.get_command(astyle_bin, "ASTYLE")
+def filter_with_astyle(input_data):
+    astyle = get_command("astyle", "ASTYLE")
     if astyle:
-        return external_tool.run_and_check(
-            [str(astyle), "--keep-one-line-blocks", "--keep-one-line-statements"], input_data
+        return run_and_check(
+            [astyle, "--keep-one-line-blocks", "--keep-one-line-statements"], input_data
         )
     else:
         return input_data
 
 
-def open_and_filter(filename: Path, apply_filters: bool) -> str:
+def open_and_filter(filename, apply_filters):
     # we ignore encoding errors and use ASCII to avoid issues when
     # opening files with different encodings (UTF-8, ISO-8859, etc)
     with open(filename, "r", encoding="ascii", errors="ignore") as f:
diff --git a/tests/fc_script/estimate_difficulty.i b/tests/fc_script/estimate_difficulty.i
index e52e49bebdeba046b2d1e5be6821c9cac219aa02..23b63dd6c5250076eb5eb8e74f0197b0bd9a12cc 100644
--- a/tests/fc_script/estimate_difficulty.i
+++ b/tests/fc_script/estimate_difficulty.i
@@ -1,6 +1,6 @@
 /* run.config
    NOFRAMAC: testing frama-c-script, not frama-c itself
-   EXECNOW: LOG @PTEST_NAME@.res LOG @PTEST_NAME@.err PTESTS_TESTING=1 %{bin:frama-c-script} estimate-difficulty --no-cloc @PTEST_FILE@ > @PTEST_NAME@.res 2> @PTEST_NAME@.err
+   EXECNOW: LOG @PTEST_NAME@.res LOG @PTEST_NAME@.err PTESTS_TESTING=1 %{bin:frama-c-script} estimate-difficulty @PTEST_FILE@ > @PTEST_NAME@.res 2> @PTEST_NAME@.err
 */
 
 // these includes are not actually used by the compiler
@@ -25,8 +25,4 @@ int main() {
   strlen(); // no warning
   ccosl(); // warning: neither code nor spec
   dprintf(); // no warning: neither code nor spec, but handled by Variadic
-  __asm__("xor ax, ax");
-  malloc(2);
-  alignof(char*);
-  _Complex cc;
 }
diff --git a/tests/fc_script/oracle/estimate_difficulty.res b/tests/fc_script/oracle/estimate_difficulty.res
index c2388cab71d69c4cb16e73185e5f625471dc75d2..30f1b80034aa927ce043a821e1aba9c17be0a536 100644
--- a/tests/fc_script/oracle/estimate_difficulty.res
+++ b/tests/fc_script/oracle/estimate_difficulty.res
@@ -1,21 +1,10 @@
 Building callgraph...
 Computing data about libc/POSIX functions...
 [recursion] found recursive cycle near estimate_difficulty.i:18: f -> f
-Estimating difficulty for 10 function calls...
-WARNING: ccosl (POSIX) has neither code nor spec in Frama-C's libc
-WARNING: setjmp (POSIX) is known to be problematic for code analysis
+Estimating difficulty for 7 function calls...
+- warning: ccosl (POSIX) has neither code nor spec in Frama-C's libc
+- warning: setjmp (POSIX) is known to be problematic for code analysis
 Function-related warnings: 2
 Estimating difficulty for 3 '#include <header>' directives...
-WARNING: included header <complex.h> is explicitly unsupported by Frama-C
+- WARNING: included header <complex.h> is explicitly unsupported by Frama-C
 Header-related warnings: 1
-Calls to dynamic allocation functions: malloc
-Checking presence of unsupported C11 features...
-WARNING: unsupported keyword(s) in estimate_difficulty.i: _Complex (1 line),  alignof (1 line)
-WARNING: code seems to contain inline assembly ('asm(...)')
-Overall difficulty score:
-asm: 1
-includes: 1
-keywords: 2
-libc: 2
-malloc: 1
-recursion: 1