diff --git a/Makefile b/Makefile
index 9af6317689253cc9dbda1c8f09bc45b47fbbd540..b05a3d65d5433327a68e21afe5e58faed796ed1f 100644
--- a/Makefile
+++ b/Makefile
@@ -257,6 +257,7 @@ DISTRIB_FILES:=\
       $(LIBC_FILES)							\
       share/analysis-scripts/analysis.mk                                \
       share/analysis-scripts/benchmark_database.py                      \
+      share/analysis-scripts/build.py                                   \
       share/analysis-scripts/build_callgraph.py                         \
       share/analysis-scripts/cmd-dep.sh                                 \
       share/analysis-scripts/concat-csv.sh                              \
@@ -274,7 +275,6 @@ DISTRIB_FILES:=\
       share/analysis-scripts/heuristic_list_functions.py                \
       share/analysis-scripts/list_files.py                              \
       share/analysis-scripts/list_functions.ml                          \
-      share/analysis-scripts/make_template.py                           \
       share/analysis-scripts/make_wrapper.py                            \
       share/analysis-scripts/normalize_jcdb.py                          \
       share/analysis-scripts/parse-coverage.sh                          \
@@ -284,6 +284,7 @@ DISTRIB_FILES:=\
       share/analysis-scripts/results_display.py                         \
       share/analysis-scripts/script_for_creduce_fatal.sh                \
       share/analysis-scripts/script_for_creduce_non_fatal.sh            \
+      share/analysis-scripts/source_filter.py                           \
       share/analysis-scripts/summary.py                                 \
       share/analysis-scripts/template.mk                                \
       $(wildcard share/emacs/*.el) share/autocomplete_frama-c           \
@@ -1973,6 +1974,8 @@ install:: install-lib-$(OCAMLBEST)
 	$(CP) \
 	  share/analysis-scripts/analysis.mk \
 	  share/analysis-scripts/benchmark_database.py \
+	  share/analysis-scripts/build.py \
+	  share/analysis-scripts/build_callgraph.py \
 	  share/analysis-scripts/cmd-dep.sh \
 	  share/analysis-scripts/concat-csv.sh \
 	  share/analysis-scripts/clone.sh \
@@ -1986,7 +1989,6 @@ install:: install-lib-$(OCAMLBEST)
 	  share/analysis-scripts/git_utils.py \
 	  share/analysis-scripts/list_files.py \
 	  share/analysis-scripts/list_functions.ml \
-	  share/analysis-scripts/make_template.py \
 	  share/analysis-scripts/make_wrapper.py \
 	  share/analysis-scripts/normalize_jcdb.py \
 	  share/analysis-scripts/parse-coverage.sh \
@@ -1995,6 +1997,7 @@ install:: install-lib-$(OCAMLBEST)
 	  share/analysis-scripts/results_display.py \
 	  share/analysis-scripts/script_for_creduce_fatal.sh \
 	  share/analysis-scripts/script_for_creduce_non_fatal.sh \
+	  share/analysis-scripts/source_filter.py \
 	  share/analysis-scripts/summary.py \
 	  share/analysis-scripts/template.mk \
 	  $(FRAMAC_DATADIR)/analysis-scripts
diff --git a/bin/frama-c-script b/bin/frama-c-script
index 4a4c1d16e9c420bf663462df85fcfaa27dd044d7..014772a4d0b9acd3bd1918dcaa00670f64dea5b9 100755
--- a/bin/frama-c-script
+++ b/bin/frama-c-script
@@ -36,6 +36,10 @@ usage() {
    echo ""
    echo "  where cmd is:"
    echo ""
+   echo "  - build [--jbdb build_commands.json] [--sources file...]"
+   echo "      Produces a GNUmakefile for Frama-C analyses."
+   echo "      Uses a build_commands.json if available."
+   echo ""
    echo "  - configure machdep"
    echo "      Runs an existing configure script to only consider files"
    echo "      in Frama-C's libc; this will hopefully disable non-essential"
@@ -91,10 +95,6 @@ usage() {
    echo "      definitions, with source location and number of statements."
    echo "      Accepts Frama-C options (e.g. -cpp-extra-args for parsing)."
    echo ""
-   echo "  - make-template [dir]"
-   echo "      Interactively prepares a template for analyses,"
-   echo "      writing it to dir/GNUmakefile [default: .frama-c]."
-   echo ""
    echo "  - make-wrapper target arg..."
    echo "      Runs 'make target arg...', parsing the output to suggest"
    echo "      useful commands in case of failure."
@@ -217,9 +217,9 @@ case "$command" in
     "help" | "-help" | "--help" | "-h")
         usage 0;
         ;;
-    "make-template")
+    "build")
         shift;
-        "${FRAMAC_SHARE}"/analysis-scripts/make_template.py "$@";
+        ${FRAMAC_SHARE}/analysis-scripts/build.py "$@";
         ;;
     "list-files")
         shift;
diff --git a/doc/userman/analysis-scripts.graphml b/doc/userman/analysis-scripts.graphml
index 2c798633e54136b20090b02b064c625b8167cfdc..1a5c42041381b8a2a670fda6d7817782016047f3 100644
--- a/doc/userman/analysis-scripts.graphml
+++ b/doc/userman/analysis-scripts.graphml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
-  <!--Created by yEd 3.19-->
+  <!--Created by yEd 3.21.1-->
   <key attr.name="Description" attr.type="string" for="graph" id="d0"/>
   <key for="port" id="d1" yfiles.type="portgraphics"/>
   <key for="port" id="d2" yfiles.type="portgeometry"/>
@@ -13,7 +13,7 @@
   <key attr.name="description" attr.type="string" for="edge" id="d9"/>
   <key for="edge" id="d10" yfiles.type="edgegraphics"/>
   <graph edgedefault="directed" id="G">
-    <data key="d0"/>
+    <data key="d0" xml:space="preserve"/>
     <node id="n0">
       <data key="d6">
         <y:ShapeNode>
@@ -64,7 +64,6 @@ OS)<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelP
     </node>
     <node id="n4" yfiles.foldertype="group">
       <data key="d4" xml:space="preserve"/>
-      <data key="d5"/>
       <data key="d6">
         <y:ProxyAutoBoundsNode>
           <y:Realizers active="0">
@@ -142,12 +141,11 @@ metrics.log<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><
     </node>
     <node id="n5" yfiles.foldertype="group">
       <data key="d4" xml:space="preserve"/>
-      <data key="d5"/>
       <data key="d6">
         <y:ProxyAutoBoundsNode>
           <y:Realizers active="0">
             <y:GroupNode>
-              <y:Geometry height="254.93886947631842" width="159.0" x="909.7975425720215" y="426.6656832358396"/>
+              <y:Geometry height="254.9388694763183" width="159.0" x="909.7975425720215" y="426.6656832358397"/>
               <y:Fill color="#F5F5F5" transparent="false"/>
               <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
               <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Noto Sans" fontSize="20" fontStyle="plain" hasLineColor="false" height="31.239974975585938" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="159.0" x="0.0" xml:space="preserve" y="0.0">&lt;target&gt;.eva </y:NodeLabel>
@@ -176,7 +174,7 @@ metrics.log<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><
               <y:Geometry height="123.69433229296885" width="149.0" x="914.7975425720215" y="465.51937416674775"/>
               <y:Fill color="#99CCFF" transparent="false"/>
               <y:BorderStyle hasColor="false" raised="false" type="line" width="1.0"/>
-              <y:NodeLabel alignment="left" autoSizePolicy="node_size" fontFamily="Noto Sans" fontSize="18" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="126.57923126220703" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="149.0" x="0.0" xml:space="preserve" y="-1.4424494846191465">alarms.csv
+              <y:NodeLabel alignment="left" autoSizePolicy="node_size" fontFamily="Noto Sans" fontSize="18" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="126.57923126220703" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="149.0" x="0.0" xml:space="preserve" y="-1.4424494846190328">alarms.csv
 eva.log
 warnings.log
 metrics.log
@@ -273,7 +271,7 @@ Results<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:Mo
     <node id="n11">
       <data key="d6">
         <y:ShapeNode>
-          <y:Geometry height="93.0" width="284.0" x="178.625" y="601.8519359999999"/>
+          <y:Geometry height="93.0" width="284.0" x="178.625" y="595.6158232777708"/>
           <y:Fill color="#C0C0C0" transparent="false"/>
           <y:BorderStyle hasColor="false" raised="false" type="line" width="1.0"/>
           <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Noto Sans" fontSize="20" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="85.71992492675781" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="264.419677734375" x="9.7901611328125" xml:space="preserve" y="3.6400375366210938">(Optional)
@@ -283,6 +281,19 @@ JSON Compilation Database
         </y:ShapeNode>
       </data>
     </node>
+    <node id="n12">
+      <data key="d6">
+        <y:ShapeNode>
+          <y:Geometry height="93.0" width="284.0" x="209.625" y="700.8797105555417"/>
+          <y:Fill color="#C0C0C0" transparent="false"/>
+          <y:BorderStyle hasColor="false" raised="false" type="line" width="1.0"/>
+          <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Noto Sans" fontSize="20" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="85.71992492675781" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="221.27976989746094" x="31.36011505126953" xml:space="preserve" y="3.6400375366210938">(Optional)
+JSON Build Database
+(build_commands.json)<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
+          <y:Shape type="rectangle"/>
+        </y:ShapeNode>
+      </data>
+    </node>
     <edge id="e0" source="n0" target="n1">
       <data key="d10">
         <y:PolyLineEdge>
@@ -373,7 +384,6 @@ JSON Compilation Database
       </data>
     </edge>
     <edge id="e5" source="n5::n1" target="n6">
-      <data key="d9"/>
       <data key="d10">
         <y:PolyLineEdge>
           <y:Path sx="74.5" sy="0.0" tx="-79.0" ty="0.0"/>
@@ -384,7 +394,6 @@ JSON Compilation Database
       </data>
     </edge>
     <edge id="e6" source="n4::n2" target="n6">
-      <data key="d9"/>
       <data key="d10">
         <y:PolyLineEdge>
           <y:Path sx="75.21875" sy="0.0" tx="-79.0" ty="0.0"/>
@@ -395,7 +404,6 @@ JSON Compilation Database
       </data>
     </edge>
     <edge id="e7" source="n4::n0" target="n8">
-      <data key="d9"/>
       <data key="d10">
         <y:PolyLineEdge>
           <y:Path sx="74.859375" sy="0.0" tx="-79.0" ty="0.0"/>
@@ -406,7 +414,6 @@ JSON Compilation Database
       </data>
     </edge>
     <edge id="e8" source="n5::n0" target="n8">
-      <data key="d9"/>
       <data key="d10">
         <y:PolyLineEdge>
           <y:Path sx="74.5" sy="0.0" tx="-79.0" ty="0.0"/>
@@ -417,7 +424,6 @@ JSON Compilation Database
       </data>
     </edge>
     <edge id="e9" source="n4::n1" target="n7">
-      <data key="d9"/>
       <data key="d10">
         <y:PolyLineEdge>
           <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
@@ -428,7 +434,6 @@ JSON Compilation Database
       </data>
     </edge>
     <edge id="e10" source="n4" target="n5">
-      <data key="d9"/>
       <data key="d10">
         <y:PolyLineEdge>
           <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
@@ -439,7 +444,6 @@ JSON Compilation Database
       </data>
     </edge>
     <edge id="e11" source="n5" target="n9">
-      <data key="d9"/>
       <data key="d10">
         <y:PolyLineEdge>
           <y:Path sx="0.7024574279785156" sy="-15.095404432332202" tx="-71.29754257202148" ty="-7.339997013875063"/>
@@ -450,7 +454,6 @@ JSON Compilation Database
       </data>
     </edge>
     <edge id="e12" source="n1" target="n9">
-      <data key="d9"/>
       <data key="d10">
         <y:PolyLineEdge>
           <y:Path sx="58.97499999999991" sy="2.6637775416665477" tx="0.0" ty="0.0">
@@ -465,7 +468,6 @@ JSON Compilation Database
       </data>
     </edge>
     <edge id="e13" source="n4::n3" target="n10">
-      <data key="d9"/>
       <data key="d10">
         <y:PolyLineEdge>
           <y:Path sx="75.21875" sy="0.0" tx="-88.5" ty="0.0"/>
@@ -476,7 +478,6 @@ JSON Compilation Database
       </data>
     </edge>
     <edge id="e14" source="n5::n2" target="n10">
-      <data key="d9"/>
       <data key="d10">
         <y:PolyLineEdge>
           <y:Path sx="74.5" sy="0.0" tx="-88.5" ty="0.0"/>
@@ -496,7 +497,6 @@ JSON Compilation Database
       </data>
     </edge>
     <edge id="e15" source="n11" target="n1">
-      <data key="d9"/>
       <data key="d10">
         <y:PolyLineEdge>
           <y:Path sx="43.375" sy="-37.35619205833348" tx="7.111959999999954" ty="1.600377541666603">
@@ -504,7 +504,22 @@ JSON Compilation Database
           </y:Path>
           <y:LineStyle color="#000000" type="line" width="1.0"/>
           <y:Arrows source="none" target="standard"/>
-          <y:EdgeLabel alignment="center" backgroundColor="#FFFFFF" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Noto Sans" fontSize="20" fontStyle="plain" hasLineColor="false" height="31.239974975585938" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="146.69981384277344" x="7.325258240567905" xml:space="preserve" y="-209.49274718831396">make-template<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="center" ratio="0.8467602870289302" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
+          <y:EdgeLabel alignment="center" backgroundColor="#FFFFFF" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Noto Sans" fontSize="20" fontStyle="plain" hasLineColor="false" height="31.239974975585938" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="51.279937744140625" x="95.43407712665595" xml:space="preserve" y="-203.2500103719077">build<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="center" ratio="0.8467602870289302" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
+          <y:BendStyle smoothed="false"/>
+        </y:PolyLineEdge>
+      </data>
+    </edge>
+    <edge id="e16" source="n12" target="n0">
+      <data key="d10">
+        <y:PolyLineEdge>
+          <y:Path sx="124.53899999999999" sy="-14.479710555541715" tx="79.0" ty="0.0">
+            <y:Point x="476.164" y="556.9"/>
+            <y:Point x="364.0" y="556.9"/>
+            <y:Point x="364.0" y="407.9523135416665"/>
+            <y:Point x="364.0" y="314.8519359999999"/>
+          </y:Path>
+          <y:LineStyle color="#000000" type="line" width="1.0"/>
+          <y:Arrows source="none" target="none"/>
           <y:BendStyle smoothed="false"/>
         </y:PolyLineEdge>
       </data>
diff --git a/doc/userman/analysis-scripts.pdf b/doc/userman/analysis-scripts.pdf
index 853e0e687779bd694aa87a1786e28038948769d2..1f8ade25fb74ecba20ff4dccf9bbe7ea9efdfd4e 100644
Binary files a/doc/userman/analysis-scripts.pdf and b/doc/userman/analysis-scripts.pdf differ
diff --git a/doc/userman/user-analysis-scripts.tex b/doc/userman/user-analysis-scripts.tex
index 459e17d0d4ef48f152bd9f82ff72ca7bc046e258..05a4b6b60d39eb91a24d6043e0bd7db42fde673a 100644
--- a/doc/userman/user-analysis-scripts.tex
+++ b/doc/userman/user-analysis-scripts.tex
@@ -77,9 +77,11 @@ and presents an illustrative diagram.
 \subsection{Necessary Build Information}
 \label{sec:necessary-build-information}
 
-The command \texttt{frama-c-script make-template} can be used to generate the
-Makefile from a template. The user must fill in the following information,
-required for running an \Value analysis:
+The command \texttt{frama-c-script build} can be used to generate the
+Makefile from a template. To run an analysis with \Value, the following
+information is necessary\footnote{If a \texttt{build\_commands.json} file is
+available, \texttt{frama-c-script build} will try to fill in some of the data
+automatically.}:
 
 \begin{description}
 \item[machdep]: architectural information about the system where the code will
@@ -164,9 +166,9 @@ in section~\ref{sec:preprocessing}). This leads to a different workflow:
   In both cases, you will obtain a \texttt{compile\_commands.json} file.
 \item Run \texttt{frama-c-script list-files}. A list of the compiled files,
   along with files defining a \texttt{main} function, will be presented.
-\item Run \texttt{frama-c-script make-template} to create a template for
-  \FramaC/\Value. Answer ``yes'' when asked about using the
-  \texttt{compile\_commands.json} file.
+\item Run \texttt{frama-c-script build} to create a template for
+  \FramaC/\Value. It should detect the \texttt{compile\_commands.json} file
+  and add the option to enable it.
 \end{enumerate}
 
 Ideally, the above approach should result in a working template. In practice,
@@ -382,10 +384,11 @@ The most useful commands are described below.
 Run \texttt{frama-c-script help} for more details and optional arguments.
 
 \begin{description}
-\item[make-template]: creates the initial Makefile, based on a template.
+\item[build]: creates the initial Makefile, based on a template.
   This command creates a file named \texttt{.frama-c/GNUmakefile} with some
-  hardcoded sections, some sections filled in interactively by the user,
-  and comments indicating which parts may need change.
+  hardcoded sections, some sections filled in according to command-line
+  options, and some sections filled automatically with build information
+  (via a \texttt{build\_commands.json} file), if available.
   Once created, it enables the general workflow mentioned earlier.
 \item[make-wrapper <target> <args>]: calls \texttt{make <target> <args>} with
   a special wrapper: when running \Value, upon encountering one of a few known
@@ -421,8 +424,7 @@ Other commands, only useful in a few cases, are described below.
 The following commands require a JSON Compilation Database.
 
 \begin{description}
-\item[list-files]: lists all files in the given JCDB. Useful for filling
-  out the Makefile template when running \texttt{make-template}.
+\item[list-files]: lists all files in the given JCDB.
 \item[normalize-jcdb]: converts absolute paths inside a
   \texttt{compile\_commands.json} file into relative paths (w.r.t. PWD)
   when possible. Used to allow moving/versioning the directory containing
diff --git a/headers/header_spec.txt b/headers/header_spec.txt
index 0d0c951d48b04dbdbd428528415e6281ccd9258c..0e623c2b82e255ecf0ec4a95f84a706cfe3503cb 100644
--- a/headers/header_spec.txt
+++ b/headers/header_spec.txt
@@ -116,6 +116,7 @@ ptests/ptests.ml: CEA_LGPL
 share/_frama-c: CEA_LGPL
 share/analysis-scripts/analysis.mk: CEA_LGPL
 share/analysis-scripts/benchmark_database.py: CEA_LGPL
+share/analysis-scripts/build.py: CEA_LGPL
 share/analysis-scripts/build_callgraph.py: CEA_LGPL
 share/analysis-scripts/clone.sh: .ignore
 share/analysis-scripts/creduce.sh: CEA_LGPL
@@ -133,7 +134,6 @@ share/analysis-scripts/git_utils.py: CEA_LGPL
 share/analysis-scripts/heuristic_list_functions.py: CEA_LGPL
 share/analysis-scripts/list_files.py: CEA_LGPL
 share/analysis-scripts/list_functions.ml: CEA_LGPL
-share/analysis-scripts/make_template.py: CEA_LGPL
 share/analysis-scripts/make_wrapper.py: CEA_LGPL
 share/analysis-scripts/normalize_jcdb.py: CEA_LGPL
 share/analysis-scripts/parse-coverage.sh: .ignore
@@ -143,6 +143,7 @@ share/analysis-scripts/README.md: .ignore
 share/analysis-scripts/results_display.py: CEA_LGPL
 share/analysis-scripts/script_for_creduce_fatal.sh: .ignore
 share/analysis-scripts/script_for_creduce_non_fatal.sh: .ignore
+share/analysis-scripts/source_filter.py: CEA_LGPL
 share/analysis-scripts/summary.py: CEA_LGPL
 share/analysis-scripts/template.mk: .ignore
 share/compliance/c11_functions.json: .ignore
diff --git a/share/analysis-scripts/build.py b/share/analysis-scripts/build.py
new file mode 100755
index 0000000000000000000000000000000000000000..768082a01d42c21ea3621d7dc92bebf382ecf79b
--- /dev/null
+++ b/share/analysis-scripts/build.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+#-*- coding: utf-8 -*-
+##########################################################################
+#                                                                        #
+#  This file is part of Frama-C.                                         #
+#                                                                        #
+#  Copyright (C) 2007-2021                                               #
+#    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 script uses blug and a build_commands.json file to produce an
+# analysis GNUmakefile, as automatically as possible.
+
+import argparse
+import json
+import logging
+import os
+from   pathlib import Path
+import re
+import shutil
+import sys
+import subprocess
+
+import function_finder
+import source_filter
+
+script_dir = os.path.dirname(sys.argv[0])
+
+# Command-line parsing ########################################################
+
+parser = argparse.ArgumentParser(description="""Produces a GNUmakefile
+for analysis with Frama-C. Tries to use a build_commands.json file if
+available.""")
+parser.add_argument('--debug', metavar='FILE',
+                    help='enable debug mode and redirect output to the specified file')
+parser.add_argument('--force', action="store_true",
+                    help='overwrite files without prompting')
+parser.add_argument('--jbdb', metavar='FILE', default="build_commands.json",
+                    help='path to JBDB (default: build_commands.json)')
+parser.add_argument('--machdep', metavar='MACHDEP',
+                    help="analysis machdep (default: Frama-C's default)")
+parser.add_argument('--main', metavar='FUNCTION', default="main",
+                    help='name of the main function (default: main)')
+parser.add_argument('--sources', metavar='FILE', nargs='+',
+                    help='list of sources to parse (overrides --jbdb)',
+                    type=Path)
+parser.add_argument('--targets', metavar='FILE', nargs='+',
+                    help='targets to build. When using --sources, ' +
+                    'only a single target is allowed.',
+                    type=Path)
+
+args = parser.parse_args()
+force = args.force
+jbdb_path = args.jbdb
+machdep = args.machdep
+main = args.main
+sources = args.sources
+targets = args.targets
+debug = args.debug
+
+debug_level = logging.DEBUG if debug else logging.INFO
+# special values for debug filename
+if debug == "stdout":
+    logging.basicConfig(stream=sys.stdout, level=debug_level,
+                        format='[%(levelname)s] %(message)s')
+elif debug == "stderr":
+    logging.basicConfig(stream=sys.stderr, level=debug_level,
+                        format='[%(levelname)s] %(message)s')
+elif debug:
+    logging.basicConfig(filename=debug, level=debug_level, filemode='w',
+                        format='[%(levelname)s] %(message)s')
+else:
+    logging.basicConfig(level=logging.INFO,
+                        format='[%(levelname)s] %(message)s')
+
+dot_framac_dir = Path(".frama-c")
+
+# Check required environment variables and commands in the PATH ###############
+
+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 = 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
+blug_print = blug_dir / "blug-print"
+# to import blug_jbdb
+sys.path.insert(0, blug_dir.as_posix())
+import blug_jbdb
+from blug_jbdb import prettify
+
+
+# Auxiliary functions #########################################################
+
+def call_and_get_output(command_and_args):
+    try:
+        return subprocess.check_output(command_and_args, stderr=subprocess.STDOUT).decode()
+    except subprocess.CalledProcessError as e:
+        sys.exit(f"error running command: {command_and_args}\n{e}")
+
+def ask_if_overwrite(path):
+    yn = input(f"warning: {path} already exists. Overwrite? [y/N] ")
+    if yn == "" or not (yn[0] == "Y" or yn[0] == "y"):
+        sys.exit("Exiting without overwriting.")
+
+def insert_lines_after(lines, line_pattern, new_lines):
+    re_line = re.compile(line_pattern)
+    for i, line in enumerate(lines):
+        if re_line.search(line):
+            for j, new_line in enumerate(new_lines):
+                lines.insert(i+1+j, new_line)
+            return
+    sys.exit(f"error: no lines found matching pattern: {line_pattern}")
+
+# delete the first occurrence of [line_pattern]
+def delete_line(lines, line_pattern):
+    re_line = re.compile(line_pattern)
+    for i, line in enumerate(lines):
+        if re_line.search(line):
+            del lines[i]
+            return
+    sys.exit(f"error: no lines found matching pattern: {line_pattern}")
+
+def replace_line(lines, line_pattern, value, all_occurrences=False):
+    replaced = False
+    re_line = re.compile(line_pattern)
+    for i, line in enumerate(lines):
+        if re_line.search(line):
+            lines[i] = value
+            replaced = True
+            if not all_occurrences:
+                return
+    if replaced:
+        return
+    sys.exit(f"error: no lines found matching pattern: {line_pattern}")
+
+# replaces '/' and '.' with '_' so that a valid target name is created
+def make_target_name(target):
+    return prettify(target).replace('/', '_').replace('.', '_')
+
+# 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):
+    return [f"  {rel_prefix(source)} \\" for source in sources]
+
+def lines_of_file(path):
+    return path.read_text().splitlines()
+
+fc_stubs_copied = False
+def copy_fc_stubs():
+    global fc_stubs_copied
+    dest = dot_framac_dir / "fc_stubs.c"
+    if not fc_stubs_copied:
+        fc_stubs = lines_of_file(share_dir / "analysis-scripts" / "fc_stubs.c")
+        re_main = re.compile(r"\bmain\b")
+        for i, line in enumerate(fc_stubs):
+            if line.startswith("//"):
+                continue
+            fc_stubs[i] = re.sub(re_main, main, line)
+        if not force and dest.exists():
+            ask_if_overwrite(dest)
+        with open(dest,"w") as f:
+            f.write("\n".join(fc_stubs))
+        logging.info("wrote: %s", dest)
+        fc_stubs_copied = True
+    return dest
+
+# Returns pairs (line_number, has_args) for each likely definition of
+# [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)
+    file_lines = file_content.splitlines(keepends=True)
+    newlines = function_finder.compute_newline_offsets(file_lines)
+    defs = function_finder.find_definitions_and_declarations(True, False, filename, file_content, file_lines, newlines, funcname)
+    res = []
+    for d in defs:
+        defining_line = file_lines[d[2]-1]
+        after_funcname = defining_line[defining_line.find(funcname)+len(funcname):]
+        # heuristics: if there is a comma after the function name,
+        # it is very likely the signature contains arguments;
+        # otherwise, the function is either defined in several lines,
+        # or we somehow missed it. By default, we assume it has no arguments
+        # if we miss it.
+        has_args = ',' in after_funcname
+        res.append((d[2], has_args))
+    return res
+
+# End of auxiliary functions ##################################################
+
+sources_map = dict()
+if sources:
+    if not targets:
+        sys.exit("error: option --targets is mandatory when --sources is specified")
+    if len(targets) > 1:
+        sys.exit("error: option --targets can only have a single target when --sources is specified")
+    sources_map[targets[0]] = [s for s in sources if blug_jbdb.filter_source(s)]
+elif os.path.isfile(jbdb_path):
+    # JBDB exists
+    with open(jbdb_path, "r") as data:
+        jbdb = json.load(data)
+    blug_jbdb.absolutize_jbdb(jbdb)
+    jbdb_targets = []
+    for f in jbdb:
+        jbdb_targets += [t for t in f["targets"] if blug_jbdb.filter_target(t)]
+    if not jbdb_targets:
+        sys.exit(f"no targets found in JBDB ({jbdb_path})")
+    if not targets:
+        # no targets specified in command line; use all from JBDB
+        targets = jbdb_targets
+    logging.info("Computing sources for each target (%d target(s))...", len(targets))
+    unknown_targets = []
+    graph = blug_jbdb.build_graph(jbdb)
+    for target in targets:
+        if target not in jbdb_targets:
+            unknown_targets.append(target)
+        else:
+            if unknown_targets != []:
+                continue # already found a problem; avoid useless computations
+            sources = [s for s in blug_jbdb.collect_leaves(graph, [target]) if blug_jbdb.filter_source(s)]
+            sources_map[target] = sorted(sources)
+    if unknown_targets:
+        targets_pretty = "\n".join(unknown_targets)
+        sys.exit("target(s) not found in JBDB:\n{targets_pretty}")
+else:
+    if not jbdb_path:
+        sys.exit("error: either a JBDB or option --sources are required")
+    else:
+        sys.exit(f"error: invalid JBDB path: '{jbdb_path}'")
+
+logging.debug("sources_map: %s", sorted([prettify(k) + ": " + ', '.join(sorted([prettify(s) for s in v])) for (k, v) in sources_map.items()]))
+logging.debug("targets: %s", sorted([prettify(t) for t in targets]))
+
+# check that source files exist
+unknown_sources = sorted({s for sources in sources_map.values() for s in sources if not s.exists()})
+if unknown_sources:
+    sys.exit("error: source(s) not found:\n" + "\n".join([prettify(s) for s in unknown_sources]))
+
+# Check that the main function is defined exactly once per target.
+# note: this is only based on heuristics (and fails on a few real case studies),
+# so we cannot emit errors, only warnings.
+# We also need to check if the main function uses a 'main(void)'-style
+# signature, to patch fc_stubs.c.
+
+main_definitions = {}
+for target, sources in sources_map.items():
+    main_definitions[target] = []
+    for source in sources:
+        fundefs = find_definitions(main, source)
+        main_definitions[target] += [(source, fundef[0], fundef[1]) for fundef in fundefs]
+    if main_definitions[target] == []:
+        logging.warning("function '%s' seems to be never defined in the sources of target '%s'", main, prettify(target))
+    elif len(main_definitions[target]) > 1:
+        logging.warning("function '%s' seems to be defined multiple times in the sources of target '%s':", main, prettify(target))
+        for (filename, line, _) in main_definitions[target]:
+            print(f"- definition at {filename}:{line}")
+
+# End of checks; start writing GNUmakefile and stubs from templates ###########
+
+if not dot_framac_dir.is_dir():
+    logging.debug("creating %s", dot_framac_dir)
+    dot_framac_dir.mkdir(parents=True, exist_ok=False)
+
+fc_config = json.loads(call_and_get_output([framac_bin / "frama-c", "-print-config-json"]))
+share_dir = Path(fc_config['datadir'])
+
+# copy fc_stubs if at least one main function has arguments
+any_has_arguments = False
+for defs in main_definitions.values():
+    if any(d[2] for d in defs):
+        any_has_arguments = True
+        break
+
+if any_has_arguments:
+    fc_stubs = copy_fc_stubs()
+    for target in targets:
+        if any(d[2] for d in main_definitions[target]):
+            logging.debug("target %s has main with args, adding fc_stubs.c to its sources", prettify(target))
+            sources_map[target].insert(0, fc_stubs)
+
+gnumakefile = dot_framac_dir / "GNUmakefile"
+
+template = lines_of_file(share_dir / "analysis-scripts" / "template.mk")
+
+if machdep:
+    machdeps = fc_config['machdeps']
+    if machdep not in machdeps:
+        logging.warning("unknown machdep (%s) not in Frama-C's default machdeps:\n%s", machdep, " ".join(machdeps))
+    replace_line(template, "^MACHDEP = .*", f"MACHDEP = {machdep}")
+
+if jbdb_path:
+    insert_lines_after(template, "^FCFLAGS", [f"  -json-compilation-database {rel_prefix(jbdb_path)} \\"])
+
+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(sources_map.items()):
+    pp_target = make_target_name(target)
+    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", prettify(target))
+        new_lines += [f"{pp_target}.parse: FCFLAGS += -main eva_main", ""]
+    insert_lines_after(template, "^### Each target <t>.eva", new_lines)
+
+gnumakefile.write_text("\n".join(template))
+
+logging.info("wrote: %s", gnumakefile)
+
+# write path.mk, but only if it does not exist.
+path_mk = dot_framac_dir / "path.mk"
+if not force and path_mk.exists():
+    logging.info("%s already exists, will not overwrite it", path_mk)
+else:
+    path_mk.write_text(f"""FRAMAC_BIN={framac_bin}
+ifeq ($(wildcard $(FRAMAC_BIN)),)
+# Frama-C not installed locally; using the version in the PATH
+else
+FRAMAC=$(FRAMAC_BIN)/frama-c
+FRAMAC_GUI=$(FRAMAC_BIN)/frama-c-gui
+endif
+""")
+    logging.info("wrote: %s", path_mk)
diff --git a/share/analysis-scripts/build_callgraph.py b/share/analysis-scripts/build_callgraph.py
index b53e0d33bb133f15413b096c368f0cc8701d3df2..a1047a70e0eaa9d92044396ed947d5178c2a0822 100755
--- a/share/analysis-scripts/build_callgraph.py
+++ b/share/analysis-scripts/build_callgraph.py
@@ -25,14 +25,14 @@
 # This script finds files containing likely declarations and definitions
 # for a given function name, via heuristic syntactic matching.
 
-import sys
 import os
 import re
+import sys
+
 import function_finder
+import source_filter
 
-MIN_PYTHON = (3, 5)
-if sys.version_info < MIN_PYTHON:
-    sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON)
+under_test = os.getenv("PTESTS_TESTING")
 
 arg = ""
 if len(sys.argv) < 2:
@@ -78,13 +78,13 @@ class Callgraph:
         return f"Callgraph({self.succs}, {self.edges})"
 
 def compute(files):
-    #print(f"Computing callgraph for {len(files)} file(s)...")
     cg = Callgraph()
     for f in files:
-        #print(f"Processing {os.path.relpath(f)}...")
-        newlines = function_finder.compute_newline_offsets(f)
-        defs = function_finder.find_definitions_and_declarations(True, False, f, newlines)
-        calls = function_finder.find_calls(f, newlines)
+        file_content = source_filter.open_and_filter(f, 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(True, False, f, file_content, file_lines, newlines)
+        calls = function_finder.find_calls(file_content, newlines)
         for call in calls:
             caller = function_finder.find_caller(defs, call)
             if caller:
@@ -93,7 +93,6 @@ def compute(files):
                 if debug:
                     print(f"build_callgraph: {f}:{line}: {caller} -> {called}")
                 cg.add_edge(caller, called, loc)
-    #print(f"Callgraph computed ({len(cg.succs)} node(s), {len(cg.edges)} edge(s))")
     return cg
 
 def print_edge(cg, caller, called, padding="", end="\n"):
diff --git a/share/analysis-scripts/detect_recursion.py b/share/analysis-scripts/detect_recursion.py
index 332d80f3b8ca50eb555c5100b68d96dc7c880e4f..c5da73ad2f3712259a6b8cde93193997ee9a45f9 100755
--- a/share/analysis-scripts/detect_recursion.py
+++ b/share/analysis-scripts/detect_recursion.py
@@ -28,10 +28,6 @@
 import sys
 import build_callgraph
 
-MIN_PYTHON = (3, 5) # for glob(recursive)
-if sys.version_info < MIN_PYTHON:
-    sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON)
-
 arg = ""
 if len(sys.argv) < 2:
     print(f"usage: {sys.argv[0]} [file1 file2 ...]")
diff --git a/share/analysis-scripts/estimate_difficulty.py b/share/analysis-scripts/estimate_difficulty.py
index dab975aabac5e7bf03d0de6a7f20bb4adac0a367..13ad4fec48a3468d47280922239ade96811b04b9 100755
--- a/share/analysis-scripts/estimate_difficulty.py
+++ b/share/analysis-scripts/estimate_difficulty.py
@@ -26,25 +26,23 @@
 # of analyzing a new code base with Frama-C.
 
 import argparse
-import build_callgraph
-import function_finder
 import json
 import os
-from pathlib import Path
+from   pathlib import Path
 import re
 import subprocess
 import sys
 import tempfile
 
+import build_callgraph
+import function_finder
+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
 
-MIN_PYTHON = (3, 5)
-if sys.version_info < MIN_PYTHON:
-    sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON)
-
 parser = argparse.ArgumentParser(description="""
 Estimates the difficulty of analyzing a given code base""")
 parser.add_argument("--header-dirs", "-d", metavar='DIR', nargs='+',
@@ -57,6 +55,8 @@ if not header_dirs:
     header_dirs = []
 files = args["files"]
 
+under_test = os.getenv("PTESTS_TESTING")
+
 # gather information from several sources
 
 def extract_keys(l):
@@ -79,16 +79,16 @@ def get_framac_libc_function_statuses(framac, framac_share):
     return (defined, spec_only)
 
 re_include = re.compile(r'\s*#\s*include\s*("|<)([^">]+)("|>)')
-def grep_includes_in_file(file):
-    with open(file, "r", encoding="utf-8", errors='ignore') as f:
-        i = 0
-        for line in f.readlines():
-            i += 1
-            m = re_include.match(line)
-            if m:
-                kind = m.group(1)
-                header = m.group(2)
-                yield((i,kind,header))
+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():
+        i += 1
+        m = re_include.match(line)
+        if m:
+            kind = m.group(1)
+            header = m.group(2)
+            yield((i,kind,header))
 
 def get_includes(files):
     quote_includes = {}
@@ -165,9 +165,10 @@ for callee in sorted(callees):
             warnings += 1
         if verbose or debug or status == "warning":
             print(f"- {status}: {callee} ({standard}) {reason}")
-    #print(f"callee: {callee}")
+    is_problematic = callee in posix_identifiers and "notes" in posix_identifiers[callee] and "fc-support" in posix_identifiers[callee]["notes"] and posix_identifiers[callee]["notes"]["fc-support"] == "problematic"
     if callee in posix_identifiers:
         used_headers.add(posix_identifiers[callee]["header"])
+    status_emitted = False # to avoid re-emitting a message for functions in both C11 and POSIX
     if callee in c11_functions:
         standard = "C11"
         # check that the callee is not a macro or type (e.g. va_arg);
@@ -175,30 +176,40 @@ for callee in sorted(callees):
         # so we must test membership before checking the POSIX type
         if callee in posix_identifiers and posix_identifiers[callee]["id_type"] != "function":
             continue
-        #print(f"C11 function: {callee}")
-        if callee in libc_specified_functions:
+        if (not is_problematic) and callee in libc_specified_functions:
             callee_status("good", standard, "is specified in Frama-C's libc")
-        elif callee in libc_defined_functions:
+            status_emitted = True
+        elif (not is_problematic) and callee in libc_defined_functions:
             callee_status("ok", standard, "is defined in Frama-C's libc")
+            status_emitted = True
         else:
-            # Some functions without specification are actually variadic
-            # (and possibly handled by the Variadic plug-in)
-            if callee in posix_identifiers and "notes" in posix_identifiers[callee] and "variadic-plugin" in posix_identifiers[callee]["notes"]:
-                callee_status("ok", standard, "is handled by the Variadic plug-in")
-            else:
+            if callee not in posix_identifiers:
                 callee_status("warning", standard, "has neither code nor spec in Frama-C's libc")
-    elif callee in posix_identifiers:
+                status_emitted = True
+    if not status_emitted and callee in posix_identifiers:
         standard = "POSIX"
         # check that the callee is not a macro or type (e.g. va_arg)
         if posix_identifiers[callee]["id_type"] != "function":
             continue
-        #print(f"Non-C11, POSIX function: {callee}")
-        if callee in libc_specified_functions:
+        if (not is_problematic) and callee in libc_specified_functions:
             callee_status("good", standard, "specified in Frama-C's libc")
-        elif callee in libc_defined_functions:
+            status_emitted = True
+        elif (not is_problematic) and callee in libc_defined_functions:
             callee_status("ok", standard, "defined in Frama-C's libc")
+            status_emitted = True
         else:
-            callee_status("warning", standard, "has neither code nor spec in Frama-C's libc")
+            # Some functions without specification are actually variadic
+            # (and possibly handled by the Variadic plug-in)
+            if "notes" in posix_identifiers[callee]:
+                if "variadic-plugin" in posix_identifiers[callee]["notes"]:
+                    callee_status("ok", standard, "is handled by the Variadic plug-in")
+                    status_emitted = True
+                elif is_problematic:
+                    callee_status("warning", standard, "is known to be problematic for code analysis")
+                    status_emitted = True
+            if not status_emitted:
+                callee_status("warning", standard, "has neither code nor spec in Frama-C's libc")
+
 print(f"Function-related warnings: {warnings}")
 
 if (verbose or debug) and used_headers:
@@ -217,12 +228,15 @@ def is_local_header(header_dirs, header):
 
 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 header in posix_headers:
         fc_support = posix_headers[header]["fc-support"]
         if fc_support == "unsupported":
+            header_warnings += 1
             print(f"- WARNING: included header <{header}> is explicitly unsupported by Frama-C")
         elif fc_support == "none":
+            header_warnings += 1
             print(f"- warning: included header <{header}> not currently included in Frama-C's libc")
         else:
             if verbose or debug:
@@ -234,8 +248,9 @@ for header in sorted(chevron_includes, key=str.casefold):
                 print(f"- ok: included header <{header}> seems to be available locally")
         else:
             non_posix_headers.append(header)
+            header_warnings += 1
             print(f"- warning: included non-POSIX header <{header}>")
-print(f"Header-related warnings: {len(non_posix_headers)}")
+print(f"Header-related warnings: {header_warnings}")
 
 
 # dynamic allocation
@@ -248,7 +263,9 @@ if dyncallees:
 
 # unsupported C11-specific features
 
-c11_unsupported = ["_Alignas", "_Alignof", "_Generic", "_Static_assert"]
+c11_unsupported = ["_Alignas", "_Alignof", "_Complex", "_Generic", "_Imaginary",
+                   "alignas", "alignof" # stdalign.h may use these symbols instead of the C11 keywords
+                   ];
 
 for keyword in c11_unsupported:
     out = subprocess.Popen(["grep", "-n", '\\b' + keyword + '\\b'] + files + ["/dev/null"],
diff --git a/share/analysis-scripts/find_fun.py b/share/analysis-scripts/find_fun.py
index b14e429fba77ebf14b0e952f5826b0ffa71a6a10..c05fbb68af1854602b05ca2587450f0c1cb501cd 100755
--- a/share/analysis-scripts/find_fun.py
+++ b/share/analysis-scripts/find_fun.py
@@ -32,10 +32,6 @@ import re
 import sys
 from pathlib import Path
 
-MIN_PYTHON = (3, 5) # for glob(recursive)
-if sys.version_info < MIN_PYTHON:
-    sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON)
-
 parser = argparse.ArgumentParser(description="""
 Looks for likely declarations/definitions of a function
 in files with extensions '.c', '.h' and '.i'.
@@ -78,12 +74,15 @@ possible_declarators = []
 possible_definers = []
 re_fun = function_finder.prepare_re_specific_name(fname)
 for f in files:
-    found = function_finder.find_specific_name(re_fun, f)
-    if found:
-        if found == 1:
-            possible_declarators.append(f)
-        else:
-            possible_definers.append(f)
+    try:
+        found = function_finder.find_specific_name(re_fun, f)
+        if found:
+            if found == 1:
+                possible_declarators.append(f)
+            else:
+                possible_definers.append(f)
+    except OSError as e:
+        print(f"error opening '{f}' ({e.errno}, {e.strerror}), skipping file")
 
 def relative_path_to(start):
     return lambda p: os.path.relpath(p, start=start)
diff --git a/share/analysis-scripts/function_finder.py b/share/analysis-scripts/function_finder.py
index e68f15ee479361b58be8bbea9284b2538b0130e5..7aef53cef0644df796ea85810fb9545c8de7c849 100755
--- a/share/analysis-scripts/function_finder.py
+++ b/share/analysis-scripts/function_finder.py
@@ -60,9 +60,9 @@ def prepare_re_specific_name(fname):
 
 # Returns 0 if not found, 1 if declaration, 2 if definition
 def find_specific_name(prepared_re, f):
-   with open(f, encoding="ascii", errors='ignore') as content_file:
-      content = content_file.read()
-      has_decl_or_def = prepared_re.search(content)
+   with open(f, encoding="ascii", errors='ignore') as data:
+      file_content = data.read()
+      has_decl_or_def = prepared_re.search(file_content)
       if has_decl_or_def is None:
           return 0
       else:
@@ -70,33 +70,35 @@ def find_specific_name(prepared_re, f):
          return 1 if is_decl else 2
 
 
-# matches function definitions
-re_fundef_or_decl = re.compile("^" + optional_type_prefix +
-                               "(" + c_identifier + ")" + whitespace +
-                               argument_list + whitespace +
-                               optional_c_id + whitespace + "(;|{)",
-                               flags=re.DOTALL | re.MULTILINE)
+# matches function definitions or declarations
+# if funcname is not None, only matches for the specified
+# function name
+def compute_re_def_or_decl(funcname):
+    id = funcname if funcname else c_identifier
+    return re.compile("^" + optional_type_prefix +
+                      "(" + id + ")" + whitespace +
+                      argument_list + whitespace +
+                      optional_c_id + whitespace + "(;|{)",
+                      flags=re.DOTALL | re.MULTILINE)
 
 # matches function calls
 re_funcall = re.compile("(" + c_identifier + ")" + whitespace + "\(")
 
 # Computes the offset (in bytes) of each '\n' in the file,
 # returning them as a list
-def compute_newline_offsets(filename):
+def compute_newline_offsets(file_lines):
     offsets = []
     current = 0
-    with open(filename, encoding="ascii", errors='ignore') as data:
-        for line in data:
-            current += len(line)
-            offsets.append(current)
+    for line in file_lines:
+        current += len(line)
+        offsets.append(current)
     return offsets
 
 # Returns the line number (starting at 1) containing the character
 # of offset [offset].
 # [offsets] is the sorted list of offsets for newline characters in the file.
 def line_of_offset(offsets, offset):
-    i = bisect.bisect_right(offsets, offset)
-    return i+1
+    return bisect.bisect_right(offsets, offset) + 1
 
 # Returns the line number (starting at 1) of each line starting with '}'
 # as its first character.
@@ -104,12 +106,19 @@ def line_of_offset(offsets, offset):
 # This is a heuristic to attempt to detect function closing braces:
 # it assumes that the first '}' (without preceding whitespace) after a
 # function definition denotes its closing brace.
-def compute_closing_braces(filename):
+def compute_closing_braces(file_lines):
     braces = []
-    with open(filename, encoding="ascii", errors='ignore') as data:
-        for i, line in enumerate(data, start=1):
-            if line.startswith("}"):
-               braces.append(i)
+    for i, line in enumerate(file_lines, start=1):
+        # note: lines contain '\n', so they are never empty
+        if line[0] == '}':
+            braces.append(i)
+    # Special heuristics: if the last line contains whitespace + '}',
+    # assume it closes a function.
+    last_line_number = len(file_lines)+1
+    if file_lines != [] and not (last_line_number in braces):
+        last_line = file_lines[-1].lstrip()
+        if len(last_line) >= 1 and last_line[0] == '}':
+            braces.append(last_line_number)
     return braces
 
 # Returns the first element of [line_numbers] greater than [n], or [None]
@@ -118,10 +127,11 @@ def compute_closing_braces(filename):
 #
 # [line_numbers] must be sorted in ascending order.
 def get_first_line_after(line_numbers, n):
-    for line in line_numbers:
-        if line > n:
-            return line
-    return None
+    try:
+        return line_numbers[bisect.bisect_left(line_numbers, n)]
+    except IndexError:
+        # could not find line (e.g. for closing braces); return None
+        return None
 
 # Returns a list of tuples (fname, is_def, line_start, line_end, terminator_offset)
 # for each function definition or declaration.
@@ -133,37 +143,38 @@ def get_first_line_after(line_numbers, n):
 # [terminator_offset] is used by the caller to filter the function prototype
 # itself and avoid considering it as a call. For function definitions,
 # this is the opening brace; for function declarations, this is the semicolon.
-def find_definitions_and_declarations(want_defs, want_decls, filename, newlines):
-    braces = compute_closing_braces(filename)
-    with open(filename, encoding="ascii", errors='ignore') as data:
-        content = data.read()
+def find_definitions_and_declarations(want_defs, want_decls, filename, file_content, file_lines, newlines, funcname=None):
+    braces = compute_closing_braces(file_lines)
     res = []
-    for match in re.finditer(re_fundef_or_decl, content):
+    re_fundef_or_decl = compute_re_def_or_decl(funcname)
+    for match in re.finditer(re_fundef_or_decl, file_content):
         funcname = match.group(1)
-        is_def = match.group(2) == "{"
-        is_decl = match.group(2) == ";"
+        terminator = match.group(2)
+        terminator_offset = match.start(2)
+        is_def = terminator == "{"
+        is_decl = terminator == ";"
         assert is_def or is_decl
         start = line_of_offset(newlines, match.start(1))
         if is_decl:
             if not want_decls:
                 continue
-            end = line_of_offset(newlines, match.start(2))
+            end = line_of_offset(newlines, terminator_offset)
         else:
             if not want_defs:
                 continue
-            definition = content[match.start(1):newlines[start-1]]
+            definition = file_content[match.start(1):newlines[start-1]]
             # try "single-line function heuristic":
             # assume the function is defined as 'type f(...) { code; }',
             # in a single line
             if definition.strip().endswith("}"):
-                end = line_of_offset(newlines, match.start(2))
+                end = line_of_offset(newlines, terminator_offset)
             else:
                 end = get_first_line_after(braces, start)
                 if not end:
                     # no closing braces found; try again the "single-line function heuristic"
                     def_start_newline_offset = newlines[start-1]
-                    line_of_opening_brace = line_of_offset(newlines, match.start(2))
-                    if start == line_of_opening_brace and definition.rstrip().endswith("}"):
+                    line_of_opening_brace = line_of_offset(newlines, terminator_offset)
+                    if start == line_of_opening_brace and definition.rstrip()[-1] == '}':
                         # assume the '}' is closing the '{' from the same line
                         end = line_of_opening_brace
                     else:
@@ -171,7 +182,6 @@ def find_definitions_and_declarations(want_defs, want_decls, filename, newlines)
                         print(f"{os.path.relpath(filename)}:{start}:closing brace not found, " +
                               f"skipping potential definition of '{funcname}'")
                         continue
-        terminator_offset = match.start(2)
         if debug:
             print(f"function_finder: {'def' if is_def else 'decl'} of {funcname} between {start} and {end}")
         res.append((funcname, is_def, start, end, terminator_offset))
@@ -184,12 +194,10 @@ calls_blacklist = ["if", "while", "for", "return", "sizeof", "switch", "_Alignas
 #
 # Note: this may include the function prototype itself;
 # it must be filtered by the caller.
-def find_calls(filename, newlines):
-    with open(filename, encoding="ascii", errors='ignore') as data:
-        content = data.read()
+def find_calls(file_content, newlines):
     # create a list of Match objects that fit "pattern" regex
     res = []
-    for match in re.finditer(re_funcall, content):
+    for match in re.finditer(re_funcall, file_content):
         funcname = match.group(1)
         offset = match.start(1)
         line = line_of_offset(newlines, offset)
diff --git a/share/analysis-scripts/heuristic_list_functions.py b/share/analysis-scripts/heuristic_list_functions.py
index ceee3fd5a3cf2892f1ed5c8fc8de5b8e76ce3c82..93c2d5c82f5c6cd5b45d5c3fcae6bf2f42579cb4 100755
--- a/share/analysis-scripts/heuristic_list_functions.py
+++ b/share/analysis-scripts/heuristic_list_functions.py
@@ -30,10 +30,6 @@ import os
 import re
 import function_finder
 
-MIN_PYTHON = (3, 5) # for glob(recursive)
-if sys.version_info < MIN_PYTHON:
-    sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON)
-
 debug = bool(os.getenv("DEBUG", False))
 
 arg = ""
@@ -55,8 +51,11 @@ want_decls = boolish_string(sys.argv[2])
 files = sys.argv[3:]
 
 for f in files:
-    newlines = function_finder.compute_newline_offsets(f)
-    defs_and_decls = function_finder.find_definitions_and_declarations(want_defs, want_decls, f, newlines)
+    with open(f, encoding="ascii", errors='ignore') as data:
+        file_content = data.read()
+    file_lines = file_content.splitlines(keepends=True)
+    newlines = function_finder.compute_newline_offsets(file_lines)
+    defs_and_decls = function_finder.find_definitions_and_declarations(want_defs, want_decls, f, file_content, file_lines, newlines)
     for (funcname, is_def, start, end, _offset) in defs_and_decls:
         if is_def:
             print(f"{os.path.relpath(f)}:{start}:{end}: {funcname} (definition)")
diff --git a/share/analysis-scripts/list_files.py b/share/analysis-scripts/list_files.py
index ac822030be89c50fcde456d387e91d6708cc50f7..e39ae6cb6d2940e8fd7a8d54f9c4ca29a477a697 100755
--- a/share/analysis-scripts/list_files.py
+++ b/share/analysis-scripts/list_files.py
@@ -33,10 +33,6 @@ import json
 import re
 from pathlib import Path
 
-MIN_PYTHON = (3, 6) # for glob(recursive) and automatic Path conversions
-if sys.version_info < MIN_PYTHON:
-    sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON)
-
 if len(sys.argv) < 2:
    # no argument, assume default name
    arg = Path("compile_commands.json")
diff --git a/share/analysis-scripts/make_template.py b/share/analysis-scripts/make_template.py
index 6e47b60c60993be74bbc2d40a32e9cd45150f880..dad3225eeced1547e9ea2e295987b5b445a2d650 100755
--- a/share/analysis-scripts/make_template.py
+++ b/share/analysis-scripts/make_template.py
@@ -36,10 +36,6 @@ from subprocess import Popen, PIPE
 from pathlib import Path
 import function_finder
 
-MIN_PYTHON = (3, 6) # for glob(recursive) and automatic Path conversions
-if sys.version_info < MIN_PYTHON:
-    sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON)
-
 if len(sys.argv) > 2:
     print(f"usage: {sys.argv[0]} [dir]")
     print("       creates a Frama-C makefile in [dir] (default: .frama-c)")
diff --git a/share/analysis-scripts/make_wrapper.py b/share/analysis-scripts/make_wrapper.py
index 7f9f5b9b517b3be2e1eacdc325dad720ee5eed69..56fb52e0f8c1ce4b1f6937dfc15d6ff9f04f6601 100755
--- a/share/analysis-scripts/make_wrapper.py
+++ b/share/analysis-scripts/make_wrapper.py
@@ -34,10 +34,6 @@ import sys
 from functools import partial
 import tempfile
 
-MIN_PYTHON = (3, 6) # for automatic Path conversions
-if sys.version_info < MIN_PYTHON:
-    sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON)
-
 # Check if GNU make is available and has the minimal required version
 # (4.0). Otherwise, this script will fail.
 # We first test with 'gmake', then 'make', then fail.
diff --git a/share/analysis-scripts/normalize_jcdb.py b/share/analysis-scripts/normalize_jcdb.py
index 041baef52d449f8fb0e73d0c39a542ecf481f4c2..70cff41349cc48c7f2d218aea4a42bdee2a5b152 100755
--- a/share/analysis-scripts/normalize_jcdb.py
+++ b/share/analysis-scripts/normalize_jcdb.py
@@ -32,10 +32,6 @@ import json
 import re
 from pathlib import Path
 
-MIN_PYTHON = (3, 6) # for automatic Path conversions
-if sys.version_info < MIN_PYTHON:
-    sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON)
-
 if len(sys.argv) < 2:
    # no argument, assume default name
    arg = Path("compile_commands.json")
diff --git a/share/analysis-scripts/print_callgraph.py b/share/analysis-scripts/print_callgraph.py
index 9c5451669dc0955757608d50db117bb2249b7923..54822dd238817f2840a48fb5a3dd82cb69134890 100755
--- a/share/analysis-scripts/print_callgraph.py
+++ b/share/analysis-scripts/print_callgraph.py
@@ -28,10 +28,6 @@
 import sys
 import build_callgraph
 
-MIN_PYTHON = (3, 5) # for glob(recursive)
-if sys.version_info < MIN_PYTHON:
-    sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON)
-
 dotfile = None
 args = sys.argv[1:]
 if "--dot" in args:
diff --git a/share/analysis-scripts/source_filter.py b/share/analysis-scripts/source_filter.py
new file mode 100644
index 0000000000000000000000000000000000000000..4156e072aee0fbf7b331d7fe8bbf18b2ae7b1a64
--- /dev/null
+++ b/share/analysis-scripts/source_filter.py
@@ -0,0 +1,90 @@
+#-*- coding: utf-8 -*-
+##########################################################################
+#                                                                        #
+#  This file is part of Frama-C.                                         #
+#                                                                        #
+#  Copyright (C) 2007-2021                                               #
+#    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 some functions to open and filter source files
+# before they are used by other scripts. These filters help improve
+# the efficiency of regex-based heuristics.
+
+# These filters require external tools, either in the PATH, or in
+# environment variables (the latter has higher priority than the former).
+# - scc (a fork including option -k), to remove C comments (variable SCC);
+# - astyle, to re-indent lines (variable ASTYLE)
+# If a tool is absent, the filter is equivalent to a no-op.
+
+# These functions receive a file object (such as produced by open(),
+# subprocess.run, or a previous filter) and return a
+# file object containing the output. They abort execution in case
+# 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
+
+# warnings about missing commands are disabled during testing
+emit_warns = os.getenv("PTESTS_TESTING") == None
+
+# Returns a Path to the command binary, or None if it is not found
+# Emits a warning the first time it looks for a command
+warned = {}
+def get_command(command, env_var_name):
+    p = os.getenv(env_var_name)
+    if not p:
+        p = shutil.which(command)
+    if not p:
+        if emit_warns and command not in warned:
+            print(f"info: optional external command '{command}' not found in PATH; consider installing it or setting environment variable {env_var_name}")
+            warned[command] = True
+        return None
+    return Path(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 run_and_check([scc, "-k"], input_data)
+    else:
+        return input_data
+
+def filter_with_astyle(input_data):
+    astyle = get_command("astyle", "ASTYLE")
+    if astyle:
+        return run_and_check([astyle, "--keep-one-line-blocks", "--keep-one-line-statements"], input_data)
+    else:
+        return input_data
+
+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:
+        data = f.read()
+    if apply_filters:
+        data = filter_with_astyle(filter_with_scc(data))
+    return data
diff --git a/share/compliance/posix_identifiers.json b/share/compliance/posix_identifiers.json
index b4badf4cb0004a8ee1a6cfd547a27f9981c0aac8..7fa5e758afd71b643f101a4c6a585f2e9b3e2539 100644
--- a/share/compliance/posix_identifiers.json
+++ b/share/compliance/posix_identifiers.json
@@ -3,7 +3,7 @@
     "source":"POSIX.1-2008 - Base Specifications, Issue 7 (IEEE Std 1003.1-2008, 2016 Edition)",
     "notes": {
         "description": "The 'notes' field is a list of characteristics related to CWEs or that may affect the 'difficulty' of analyzing such headers or functions with Frama-C. Each note is succintly described below.",
-        "fc-support":"How well Frama-C supports this construct, w.r.t. the C11 standard: 'unsupported', 'none', 'partial', or 'good'. The difference between 'unsupported' and 'none' is that the former means something requiring substantial changes to be supported by Frama-C, while the latter only means 'not tried yet'.",
+        "fc-support":"How well Frama-C supports this construct: 'unsupported', 'none', 'problematic', 'partial', or 'good'. The difference between 'unsupported' and 'none' is that the former means something requiring substantial changes to be supported by Frama-C, while the latter only means 'not tried yet'. 'problematic' indicates an identifier which is known to cause issues to code analyzers, e.g. setjmp.",
         "variadic-plugin":"This is a variadic function known by the Variadic plug-in, which can (in some cases) generate a specification for it."
     },
     "id_types":[
@@ -58,7 +58,7 @@
         "sched.h": {"fc-support":"partial", "extensions":[]},
         "search.h": {"fc-support":"none", "extensions":["XSI"]},
         "semaphore.h": {"fc-support":"partial", "extensions":[]},
-        "setjmp.h": {"fc-support":"partial", "extensions":[]},
+        "setjmp.h": {"fc-support":"partial", "extensions":[], "notes":{"fc-support":"problematic"}},
         "signal.h": {"fc-support":"partial", "extensions":[]},
         "spawn.h": {"fc-support":"none", "extensions":["SPN"]},
         "stdarg.h": {"fc-support":"good", "extensions":[]},
@@ -187,7 +187,7 @@
         "_IOFBF": {"id_type":"macro", "header":"stdio.h", "extensions":[]},
         "_IOLBF": {"id_type":"macro", "header":"stdio.h", "extensions":[]},
         "_IONBF": {"id_type":"macro", "header":"stdio.h", "extensions":[]},
-        "_longjmp": {"id_type":"function", "header":"setjmp.h", "extensions":["OB","XSI"]},
+        "_longjmp": {"id_type":"function", "header":"setjmp.h", "extensions":["OB","XSI"], "notes":{"fc-support":"problematic"}},
         "_PC_2_SYMLINKS": {"id_type":"constant", "header":"unistd.h", "extensions":[]},
         "_PC_ALLOC_SIZE_MIN": {"id_type":"constant", "header":"unistd.h", "extensions":[]},
         "_PC_ASYNC_IO": {"id_type":"constant", "header":"unistd.h", "extensions":[]},
@@ -468,7 +468,7 @@
         "_SC_XOPEN_UUCP": {"id_type":"constant", "header":"unistd.h", "extensions":[]},
         "_SC_XOPEN_VERSION": {"id_type":"constant", "header":"unistd.h", "extensions":[]},
         "_SC_XOPEN_XCU_VERSION": {"id_type":"constant", "header":"unistd.h", "extensions":["REM"]},
-        "_setjmp": {"id_type":"function", "header":"setjmp.h", "extensions":["OB","XSI"]},
+        "_setjmp": {"id_type":"function", "header":"setjmp.h", "extensions":["OB","XSI"], "notes":{"fc-support":"problematic"}},
         "_tolower": {"id_type":"function", "header":"ctype.h", "extensions":["OB","XSI"]},
         "_toupper": {"id_type":"function", "header":"ctype.h", "extensions":["OB","XSI"]},
         "_V6_ILP32_OFF32": {"id_type":"constant", "header":"unistd.h", "extensions":["REM"]},
@@ -1684,7 +1684,7 @@
         "IXON": {"id_type":"constant", "header":"termios.h", "extensions":[]},
         "j0": {"id_type":"function", "header":"math.h", "extensions":["XSI"]},
         "j1": {"id_type":"function", "header":"math.h", "extensions":["XSI"]},
-        "jmp_buf": {"id_type":"type", "header":"setjmp.h", "extensions":[]},
+        "jmp_buf": {"id_type":"type", "header":"setjmp.h", "extensions":[], "notes":{"fc-support":"problematic"}},
         "jn": {"id_type":"function", "header":"math.h", "extensions":["XSI"]},
         "jrand48": {"id_type":"function", "header":"stdlib.h", "extensions":["XSI"]},
         "key_t": {"id_type":"type", "header":"sys/types.h", "extensions":[]},
@@ -1823,7 +1823,7 @@
         "LONG_BIT": {"id_type":"macro", "header":"limits.h", "extensions":[]},
         "LONG_MAX": {"id_type":"macro", "header":"limits.h", "extensions":[]},
         "LONG_MIN": {"id_type":"macro", "header":"limits.h", "extensions":[]},
-        "longjmp": {"id_type":"function", "header":"setjmp.h", "extensions":[]},
+        "longjmp": {"id_type":"function", "header":"setjmp.h", "extensions":[], "notes":{"fc-support":"problematic"}},
         "lrand48": {"id_type":"function", "header":"stdlib.h", "extensions":["XSI"]},
         "lrint": {"id_type":"function", "header":"math.h", "extensions":[]},
         "lrintf": {"id_type":"function", "header":"math.h", "extensions":[]},
@@ -2910,7 +2910,7 @@
         "setgrent": {"id_type":"function", "header":"grp.h", "extensions":["XSI"]},
         "sethostent": {"id_type":"function", "header":"netdb.h", "extensions":[]},
         "setitimer": {"id_type":"function", "header":"sys/time.h", "extensions":[]},
-        "setjmp": {"id_type":"function", "header":"setjmp.h", "extensions":[]},
+        "setjmp": {"id_type":"function", "header":"setjmp.h", "extensions":[], "notes":{"fc-support":"problematic"}},
         "setkey": {"id_type":"function", "header":"stdlib.h", "extensions":["XSI"]},
         "setlocale": {"id_type":"function", "header":"locale.h", "extensions":[]},
         "setlogmask": {"id_type":"function", "header":"syslog.h", "extensions":[]},
@@ -3010,9 +3010,9 @@
         "SIGINT": {"id_type":"macro", "header":"signal.h", "extensions":[]},
         "siginterrupt": {"id_type":"function", "header":"signal.h", "extensions":[]},
         "sigismember": {"id_type":"function", "header":"signal.h", "extensions":[]},
-        "sigjmp_buf": {"id_type":"type", "header":"setjmp.h", "extensions":["CX"]},
+        "sigjmp_buf": {"id_type":"type", "header":"setjmp.h", "extensions":["CX"], "notes":{"fc-support":"problematic"}},
         "SIGKILL": {"id_type":"macro", "header":"signal.h", "extensions":[]},
-        "siglongjmp": {"id_type":"function", "header":"setjmp.h", "extensions":["CX"]},
+        "siglongjmp": {"id_type":"function", "header":"setjmp.h", "extensions":["CX"], "notes":{"fc-support":"problematic"}},
         "sigmask": {"id_type":"function", "header":"signal.h", "extensions":["REM"]},
         "signal": {"id_type":"function", "header":"signal.h", "extensions":[]},
         "signbit": {"id_type":"macro", "header":"math.h", "extensions":[]},
@@ -3032,7 +3032,7 @@
         "SIGSEGV": {"id_type":"macro", "header":"signal.h", "extensions":[]},
         "sigset": {"id_type":"function", "header":"signal.h", "extensions":[]},
         "sigset_t": {"id_type":"type", "header":"signal.h", "extensions":[]},
-        "sigsetjmp": {"id_type":"function", "header":"setjmp.h", "extensions":["CX"]},
+        "sigsetjmp": {"id_type":"function", "header":"setjmp.h", "extensions":["CX"], "notes":{"fc-support":"problematic"}},
         "sigstack": {"id_type":"type", "header":"signal.h", "extensions":["REM"]},
         "SIGSTKSZ": {"id_type":"macro", "header":"signal.h", "extensions":[]},
         "SIGSTOP": {"id_type":"macro", "header":"signal.h", "extensions":[]},
diff --git a/tests/fc_script/build-callgraph.i b/tests/fc_script/build-callgraph.i
index 7ec5a976aeb0b98d696992b666850cb7c6e497fa..be8f7b96630a6bac9c200980026fd29f01ddb347 100644
--- a/tests/fc_script/build-callgraph.i
+++ b/tests/fc_script/build-callgraph.i
@@ -1,6 +1,6 @@
 /* run.config
    NOFRAMAC: testing frama-c-script, not frama-c itself
-   EXECNOW: LOG build-callgraph.res LOG build-callgraph.err %{bin:frama-c-script} heuristic-print-callgraph @PTEST_DIR@/@PTEST_NAME@.i > @PTEST_RESULT@/build-callgraph.res 2> @PTEST_RESULT@/build-callgraph.err
+   EXECNOW: LOG build-callgraph.res LOG build-callgraph.err PTESTS_TESTING=1 %{bin:frama-c-script} heuristic-print-callgraph @PTEST_DIR@/@PTEST_NAME@.i > @PTEST_RESULT@/build-callgraph.res 2> @PTEST_RESULT@/build-callgraph.err
  */
 
 #include <stdio.h>
diff --git a/tests/fc_script/estimate_difficulty.i b/tests/fc_script/estimate_difficulty.i
new file mode 100644
index 0000000000000000000000000000000000000000..23b63dd6c5250076eb5eb8e74f0197b0bd9a12cc
--- /dev/null
+++ b/tests/fc_script/estimate_difficulty.i
@@ -0,0 +1,28 @@
+/* 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 @PTEST_FILE@ > @PTEST_NAME@.res 2> @PTEST_NAME@.err
+*/
+
+// these includes are not actually used by the compiler
+// (this is a preprocessed file), but analyzed by the script
+#include <sys/socket.h>
+# include <complex.h>
+  #	include <langinfo.h>
+#include <sys/socket.h>
+
+void g() {
+  int g = 42;
+}
+
+void f() {
+  if (v) f();
+  else g();
+}
+
+int main() {
+  va_arg(); // no warning: it is a macro, not a function
+  setjmp(); // warning: problematic
+  strlen(); // no warning
+  ccosl(); // warning: neither code nor spec
+  dprintf(); // no warning: neither code nor spec, but handled by Variadic
+}
diff --git a/tests/fc_script/list_functions.i b/tests/fc_script/list_functions.i
index 448f97a30909dda8da6e3d773ed34336bf46e4a6..225f70d0a90bc31504e675d9bbaa7bc70503d7f8 100644
--- a/tests/fc_script/list_functions.i
+++ b/tests/fc_script/list_functions.i
@@ -15,5 +15,5 @@
  DEPS: @PTEST_DEPS@ @PTEST_DIR@/build-callgraph.i
  DEPS: @PTEST_DEPS@ @PTEST_DIR@/recursions.i
 
-   EXECNOW: LOG heuristic_list_functions.res LOG heuristic_list_functions.err %{bin:frama-c-script} heuristic-list-functions true true @PTEST_DEPS@ > @PTEST_RESULT@/heuristic_list_functions.res 2> @PTEST_RESULT@/heuristic_list_functions.err
+   EXECNOW: LOG heuristic_list_functions.res LOG heuristic_list_functions.err PTESTS_TESTING=1 %{bin:frama-c-script} heuristic-list-functions true true @PTEST_DEPS@ > @PTEST_RESULT@/heuristic_list_functions.res 2> @PTEST_RESULT@/heuristic_list_functions.err
  */
diff --git a/tests/fc_script/main.c b/tests/fc_script/main.c
index d6526f2b234c2d45c42862eaf1d7c231c85f268f..cb1a6f9adc2a5443691c4809e7721d42a368ee3e 100644
--- a/tests/fc_script/main.c
+++ b/tests/fc_script/main.c
@@ -1,16 +1,15 @@
 /* run.config
  NOFRAMAC: testing frama-c-script, not frama-c itself
- DEPS: for-find-fun2.c for-find-fun.c main.c main2.c main3.c
-   EXECNOW: LOG GNUmakefile LOG make_template.res LOG make_template.err PTESTS_TESTING= %{bin:frama-c-script} -C @PTEST_DIR@ make-template $(basename @PTEST_RESULT@) < %{dep:@PTEST_DIR@/make_template.input} > @PTEST_RESULT@/make_template.res 2> @PTEST_RESULT@/make_template.err
+ COMMENT: the 'build' command cannot be tested because it requires 'glub'.
  DEPS: main2.c main3.c main.c
-   EXECNOW: LOG list_files.res LOG list_files.err %{bin:frama-c-script} list-files %{dep:@PTEST_DIR@/list_files.json} > @PTEST_RESULT@/list_files.res 2> @PTEST_RESULT@/list_files.err
+   EXECNOW: LOG list_files.res LOG list_files.err PTESTS_TESTING=1 %{bin:frama-c-script} list-files %{dep:@PTEST_DIR@/list_files.json} > @PTEST_RESULT@/list_files.res 2> @PTEST_RESULT@/list_files.err
  DEPS: for-find-fun2.c for-find-fun.c for-list-functions.c main2.c main3.c main.c make-wrapper2.c make-wrapper3.c make-wrapper.c
-   EXECNOW: LOG find_fun1.res LOG find_fun1.err %{bin:frama-c-script} find-fun main2 @PTEST_DIR@ > @PTEST_RESULT@/find_fun1.res 2> @PTEST_RESULT@/find_fun1.err
-   EXECNOW: LOG find_fun2.res LOG find_fun2.err %{bin:frama-c-script} find-fun main3 @PTEST_DIR@ > @PTEST_RESULT@/find_fun2.res 2> @PTEST_RESULT@/find_fun2.err
-   EXECNOW: LOG find_fun3.res LOG find_fun3.err %{bin:frama-c-script} find-fun false_positive @PTEST_DIR@ > @PTEST_RESULT@/find_fun3.res 2> @PTEST_RESULT@/find_fun3.err
+   EXECNOW: LOG find_fun1.res LOG find_fun1.err PTESTS_TESTING=1 %{bin:frama-c-script} find-fun main2 @PTEST_DIR@ > @PTEST_RESULT@/find_fun1.res 2> @PTEST_RESULT@/find_fun1.err
+   EXECNOW: LOG find_fun2.res LOG find_fun2.err PTESTS_TESTING=1 %{bin:frama-c-script} find-fun main3 @PTEST_DIR@ > @PTEST_RESULT@/find_fun2.res 2> @PTEST_RESULT@/find_fun2.err
+   EXECNOW: LOG find_fun3.res LOG find_fun3.err PTESTS_TESTING=1 %{bin:frama-c-script} find-fun false_positive @PTEST_DIR@ > @PTEST_RESULT@/find_fun3.res 2> @PTEST_RESULT@/find_fun3.err
  DEPS:
-   EXECNOW: LOG list_functions.res LOG list_functions.err %{bin:frama-c-script} list-functions %{dep:@PTEST_DIR@/for-find-fun2.c} %{dep:@PTEST_DIR@/for-list-functions.c} > @PTEST_RESULT@/list_functions.res 2> @PTEST_RESULT@/list_functions.err
-   EXECNOW: LOG list_functions2.res LOG list_functions2.err LOG list_functions2.json %{bin:frama-c-script} list-functions %{dep:@PTEST_DIR@/for-find-fun2.c} %{dep:@PTEST_DIR@/for-list-functions.c -list-functions-declarations} -list-functions-output @PTEST_RESULT@/list_functions2.json -list-functions-debug 1 > @PTEST_RESULT@/list_functions2.res 2> @PTEST_RESULT@/list_functions2.err
+   EXECNOW: LOG list_functions.res LOG list_functions.err PTESTS_TESTING=1 %{bin:frama-c-script} list-functions %{dep:@PTEST_DIR@/for-find-fun2.c} %{dep:@PTEST_DIR@/for-list-functions.c} > @PTEST_RESULT@/list_functions.res 2> @PTEST_RESULT@/list_functions.err
+   EXECNOW: LOG list_functions2.res LOG list_functions2.err LOG list_functions2.json PTESTS_TESTING=1 %{bin:frama-c-script} list-functions %{dep:@PTEST_DIR@/for-find-fun2.c} %{dep:@PTEST_DIR@/for-list-functions.c -list-functions-declarations} -list-functions-output @PTEST_RESULT@/list_functions2.json -list-functions-debug 1 > @PTEST_RESULT@/list_functions2.res 2> @PTEST_RESULT@/list_functions2.err
  */
 
 
diff --git a/tests/fc_script/make-wrapper.c b/tests/fc_script/make-wrapper.c
index 89700bc4aa9ecaac4668c8010ebc73edec3d2c26..0f7d0709e6eda239b3a706424d82b2f3b5d9cf00 100644
--- a/tests/fc_script/make-wrapper.c
+++ b/tests/fc_script/make-wrapper.c
@@ -2,7 +2,7 @@
 MACRO: RM_TMP_DIR rm -rf make-for-make-wrapper.parse make-for-make-wrapper.eva
    NOFRAMAC: testing frama-c-script
    COMMENT: in case of errors, remove the 'grep' part to get the full output
-   EXECNOW: LOG make-wrapper.res LOG make-wrapper.err (cd @PTEST_DIR@ && touch make-wrapper2.c && touch make-wrapper3.c && @RM_TMP_DIR@ && FRAMAC=%{bin:frama-c} %{bin:frama-c-script} make-wrapper --make-dir . -f make-for-make-wrapper.mk | grep -A999999 "make-wrapper recommendations" && @RM_TMP_DIR@) > @PTEST_RESULT@/make-wrapper.res 2> @PTEST_RESULT@/make-wrapper.err
+   EXECNOW: LOG make-wrapper.res LOG make-wrapper.err (cd @PTEST_DIR@ && touch make-wrapper2.c && touch make-wrapper3.c && @RM_TMP_DIR@ && FRAMAC=%{bin:frama-c} PTESTS_TESTING=1 %{bin:frama-c-script} make-wrapper --make-dir . -f make-for-make-wrapper.mk | grep -A999999 "make-wrapper recommendations" && @RM_TMP_DIR@) > @PTEST_RESULT@/make-wrapper.res 2> @PTEST_RESULT@/make-wrapper.err
 */
 int defined(int a);
 
diff --git a/tests/fc_script/make_template.input b/tests/fc_script/make_template.input
deleted file mode 100644
index 6963df94935110ccdf0218b25973e3348c9e1947..0000000000000000000000000000000000000000
--- a/tests/fc_script/make_template.input
+++ /dev/null
@@ -1,10 +0,0 @@
-y
-fc_script_main
-for-find*.c main*.c
-y
-y
-y
-invalid_machdep
-n
-x86_64
-y
diff --git a/tests/fc_script/oracle/make_template.err b/tests/fc_script/oracle/estimate_difficulty.err
similarity index 100%
rename from tests/fc_script/oracle/make_template.err
rename to tests/fc_script/oracle/estimate_difficulty.err
diff --git a/tests/fc_script/oracle/estimate_difficulty.res b/tests/fc_script/oracle/estimate_difficulty.res
new file mode 100644
index 0000000000000000000000000000000000000000..6b197b724146d7ba3507c29a2979df06f1027627
--- /dev/null
+++ b/tests/fc_script/oracle/estimate_difficulty.res
@@ -0,0 +1,11 @@
+Building callgraph...
+Computing data about libc/POSIX functions...
+[recursion] found recursive cycle near estimate_difficulty.i:18: f -> f
+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 <langinfo.h> not currently included in Frama-C's libc
+Header-related warnings: 2
diff --git a/tests/fc_script/oracle/find_fun1.res b/tests/fc_script/oracle/find_fun1.res
index f16fc61d2be3d40fae3e6b45961875fc0e7f0642..58bf804e3c123e33f21c6761b63dcefa26e2e129 100644
--- a/tests/fc_script/oracle/find_fun1.res
+++ b/tests/fc_script/oracle/find_fun1.res
@@ -1,4 +1,4 @@
-Looking for 'main2' inside 14 file(s)...
+Looking for 'main2' inside 15 file(s)...
 Possible declarations for function 'main2' in the following file(s):
   for-find-fun.c
 Possible definitions for function 'main2' in the following file(s):
diff --git a/tests/fc_script/oracle/find_fun2.res b/tests/fc_script/oracle/find_fun2.res
index fda8345bfc224212d8b8d653d325ed83b7acac7c..d868795ee4e11b8858e75185cfd91d68ea8a24e7 100644
--- a/tests/fc_script/oracle/find_fun2.res
+++ b/tests/fc_script/oracle/find_fun2.res
@@ -1,4 +1,4 @@
-Looking for 'main3' inside 14 file(s)...
+Looking for 'main3' inside 15 file(s)...
 Possible declarations for function 'main3' in the following file(s):
   for-find-fun2.c
 Possible definitions for function 'main3' in the following file(s):
diff --git a/tests/fc_script/oracle/find_fun3.res b/tests/fc_script/oracle/find_fun3.res
index 4a42d9117089960eb086c47a8e4b07b44795e3c3..00f4bbc10eab9c20620426e670d9d0cf6b715762 100644
--- a/tests/fc_script/oracle/find_fun3.res
+++ b/tests/fc_script/oracle/find_fun3.res
@@ -1,2 +1,2 @@
-Looking for 'false_positive' inside 14 file(s)...
+Looking for 'false_positive' inside 15 file(s)...
 No declaration/definition found for function 'false_positive'
diff --git a/tests/fc_script/oracle/heuristic_list_functions.res b/tests/fc_script/oracle/heuristic_list_functions.res
index c07b868d86a6faa808fac543c9825b4288dc37cd..b9cfea105053534f7a392710bc375f1b5bf61e1b 100644
--- a/tests/fc_script/oracle/heuristic_list_functions.res
+++ b/tests/fc_script/oracle/heuristic_list_functions.res
@@ -7,7 +7,7 @@ for-find-fun2.c:19:21: h (definition)
 for-find-fun2.c:28:31: static_fun (definition)
 for-list-functions.c:8:15: static_fun (definition)
 for-list-functions.c:17:22: k (definition)
-main.c:18:20: main (definition)
+main.c:17:19: main (definition)
 main2.c:6:8: fake_main (definition)
 main2.c:10:12: domain (definition)
 main2.c:14:16: main2 (definition)
diff --git a/tests/fc_script/oracle/make_template.res b/tests/fc_script/oracle/make_template.res
deleted file mode 100644
index 4a3ef8e2f6c94cc2d096721b2e93d5f759fc82b3..0000000000000000000000000000000000000000
--- a/tests/fc_script/oracle/make_template.res
+++ /dev/null
@@ -1,16 +0,0 @@
-Preparing template: GNUmakefile
-Running ptests: setting up mock files...
-warning: GNUmakefile already exists. Overwrite? [y/N] Main target name: Source files (default: **/*.c): The following sources were matched (relative to .):
-  ./for-find-fun.c
-  ./for-find-fun2.c
-  ./main.c	# defines 'main'
-  ./main2.c
-  ./main3.c	# defines 'main'
-
-warning: 'main' seems to be defined multiple times.
-Is this ok? [Y/n] compile_commands.json exists, add option -json-compilation-database? [Y/n] Add stub for function main (only needed if it uses command-line arguments)? [y/N] Please define the architectural model (machdep) of the target machine.
-Known machdeps: x86_16 x86_32 x86_64 gcc_x86_16 gcc_x86_32 gcc_x86_64 ppc_32 msvc_x86_64
-Please enter the machdep [x86_64]: 'invalid_machdep' is not a standard machdep. Proceed anyway? [y/N]Please enter the machdep [x86_64]: warning: fc_stubs.c already exists. Overwrite? [y/N] Created stub for main function: fc_stubs.c
-Template created: GNUmakefile
-Path to Frama-C binaries written to: path.mk
-Running ptests: cleaning up after tests...
diff --git a/tests/fc_script/recursions.i b/tests/fc_script/recursions.i
index 7d7cbfe0daab55000e646e4f72ada4a7a4d6525a..1d1f993cf17245094b713370b8ab32bcc941f2e4 100644
--- a/tests/fc_script/recursions.i
+++ b/tests/fc_script/recursions.i
@@ -1,6 +1,6 @@
 /* run.config
    NOFRAMAC: testing frama-c-script, not frama-c itself
-   EXECNOW: LOG recursions.res LOG recursions.err %{bin:frama-c-script} heuristic-detect-recursion @PTEST_FILE@ > @PTEST_RESULT@/recursions.res 2> @PTEST_RESULT@/recursions.err
+   EXECNOW: LOG recursions.res LOG recursions.err PTESTS_TESTING=1 %{bin:frama-c-script} heuristic-detect-recursion @PTEST_FILE@ > @PTEST_RESULT@/recursions.res 2> @PTEST_RESULT@/recursions.err
 */
 
 volatile int v;