diff --git a/Makefile b/Makefile index 4df5cf329a46333da072ec0bcfb9db04d8e5ffe6..c8eee3952170faf9f3cc72f2852c29860fbfc60e 100644 --- a/Makefile +++ b/Makefile @@ -264,6 +264,7 @@ DISTRIB_FILES:=\ share/analysis-scripts/frama_c_results.py \ share/analysis-scripts/git_utils.py \ share/analysis-scripts/list_files.py \ + share/analysis-scripts/make_template.py \ share/analysis-scripts/make_wrapper.py \ share/analysis-scripts/parse-coverage.sh \ share/analysis-scripts/README.md \ diff --git a/bin/frama-c-script b/bin/frama-c-script index 17c2872620774db6c5d46115422e0ecc2b12542f..3543aaee31f3e85e2215b71ac50e13946c444888 100755 --- a/bin/frama-c-script +++ b/bin/frama-c-script @@ -120,62 +120,6 @@ open_file() { esac } -make_template() { - if [ "$#" -gt 0 ]; then - dir="${1%/}" - else - dir="." - fi - path="$dir/GNUmakefile" - path_tmp="${path}.tmp" - check_path_exists "$path" - read -p "Main target name: " main - if [[ ! ( "$main" =~ ^[a-zA-Z_0-9]+$ ) ]]; then - echo "error: invalid main target name"; - exit 1 - fi - read -p "Source files separated by spaces (default if empty: *.c): " sources - if [ "$sources" = "" ]; then - sources="*.c" - fi - - add_main_stub=0 - read -p "Add stub for function main (only needed if it uses command-line arguments)? [y/N] " yn - case $yn in - [Yy]) - add_main_stub=1 - sources="fc_stubs.c $sources" - ;; - *) - ;; - esac - - sed "s/^MAIN_TARGET :=/MAIN_TARGET := ${main}/" "${FRAMAC_SHARE}/analysis-scripts/template.mk" > "$path" - sed "s|\$(MAIN_TARGET).parse:|\$(MAIN_TARGET).parse: $sources|" "$path" | sed "/# Remove these lines after defining the main target/{N;N;N;N;d;}" > "$path_tmp"; mv "$path_tmp" "$path" - if [ -e "compile_commands.json" ]; then - read -p "compile_commands.json exists, add option -json-compilation-database? [Y/n] " yn - case $yn in - [Nn]) - echo "Option not added; you can later add it to FCFLAGS." - ;; - *) - # the command below inserts a line after matching a given - # pattern; compatible with macOS sed - sed -e '/^FCFLAGS[[:space:]]\++=/a\ -\ \ -json-compilation-database .\\' "$path" > "$path_tmp"; mv "$path_tmp" "$path" - ;; - esac - fi - if [ $add_main_stub -eq 1 ]; then - check_path_exists "fc_stubs.c" - cp "${FRAMAC_SHARE}/analysis-scripts/fc_stubs.c" "./" - echo "Created stub for main function: fc_stubs.c" - sed -e '/^FCFLAGS[[:space:]]\++=/a\ -\ \ -main eva_main \\' "$path" > "$path_tmp"; mv "$path_tmp" "$path" - fi - echo "Template created: $path" -} - make_path() { cat <<EOF > frama-c-path.mk FRAMAC_DIR=${DIR} @@ -288,7 +232,7 @@ case "$command" in ;; "make-template") shift; - make_template "$@"; + ${FRAMAC_SHARE}/analysis-scripts/make_template.py "$0" "$@"; ;; "make-path") shift; diff --git a/headers/header_spec.txt b/headers/header_spec.txt index 166fbddd62c2cbc23bcf5798fb293433cbd20083..e54f95810601d05af5828bf7ea15f183ce797813 100644 --- a/headers/header_spec.txt +++ b/headers/header_spec.txt @@ -130,6 +130,7 @@ share/analysis-scripts/find_fun.py: .ignore share/analysis-scripts/flamegraph.pl: CDDL share/analysis-scripts/git_utils.py: .ignore share/analysis-scripts/list_files.py: .ignore +share/analysis-scripts/make_template.py: .ignore share/analysis-scripts/make_wrapper.py: .ignore share/analysis-scripts/parse-coverage.sh: .ignore share/analysis-scripts/README.md: .ignore diff --git a/share/analysis-scripts/make_template.py b/share/analysis-scripts/make_template.py new file mode 100755 index 0000000000000000000000000000000000000000..ca241cc8f0ebf660365c6369a52385506d19bd24 --- /dev/null +++ b/share/analysis-scripts/make_template.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +#-*- coding: utf-8 -*- +########################################################################## +# # +# This file is part of Frama-C. # +# # +# Copyright (C) 2007-2018 # +# 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 is used to interactively fill template.mk, converting it +# into a GNUmakefile ready for analysis. + +import sys +import os +import re +from subprocess import Popen, PIPE +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) + +if len(sys.argv) > 3: + print("usage: %s path-to-frama-c-script [dir]" % sys.argv[0]) + print(" creates a GNUmakefile for running Frama-C on a set of files,") + print(" interactively filling a template.") + sys.exit(1) + +if not os.path.isfile(sys.argv[1]): + print("error: path to frama-c-script is not a file: " + sys.argv[1]) + sys.exit(1) + +jcdb = Path("compile_commands.json") + +if "PTESTS_TESTING" in os.environ: + print("Running ptests: setting up mock files...") + jcdb.touch() + +bindir = Path(os.path.dirname(os.path.abspath(sys.argv[1]))) +frama_c_config = bindir / "frama-c-config" +process = Popen([frama_c_config, "-print-share-path"], stdout=PIPE) +(output, err) = process.communicate() +output = output.decode('utf-8') +exit_code = process.wait() +if exit_code != 0: + print("error running frama-c-config") + sys.exit(1) +sharedir = Path(output) + +def get_known_machdeps(): + process = Popen([bindir / "frama-c", "-machdep", "help"], stdout=PIPE) + (output, err) = process.communicate() + output = output.decode('utf-8') + exit_code = process.wait() + if exit_code != 0: + print("error getting machdeps: " + output) + sys.exit(1) + match = re.match("\[kernel\] supported machines are (.*) \(default is (.*)\).", output, re.DOTALL) + if not match: + print("error getting known machdeps: " + output) + sys.exit() + machdeps = match.group(1).split() + default_machdep = match.group(2) + return (default_machdep, machdeps) + +dir = Path(sys.argv[2] if len(sys.argv) == 3 else ".") +gnumakefile = dir / "GNUmakefile" + +def check_path_exists(path): + if os.path.exists(path): + yn = input("warning: {} already exists. Overwrite? [y/N] ".format(path)) + if yn == "" or not (yn[0] == "Y" or yn[0] == "y"): + print("Exiting without overwriting.") + sys.exit(0) + +check_path_exists(gnumakefile) +main = input("Main target name: ") +if not re.match("^[a-zA-Z_0-9]+$", main): + print("error: invalid main target name") + sys.exit(1) + +sources = input("Source files separated by spaces (default if empty: *.c): ") +if not sources: + sources="*.c" + +json_compilation_database = None +if jcdb.is_file(): + yn = input("compile_commands.json exists, add option -json-compilation-database? [Y/n] ") + if yn == "" or not (yn[0] == "N" or yn[0] == "n"): + json_compilation_database = "." + else: + print("Option not added; you can later add it to FCFLAGS.") + +add_main_stub = False +yn = input("Add stub for function main (only needed if it uses command-line arguments)? [y/N] ") +if yn != "" and (yn[0] == "Y" or yn[0] == "y"): + add_main_stub = True + sources = "fc_stubs.c " + sources + +print("Please define the architectural model (machdep) of the target machine.") +(default_machdep, machdeps) = get_known_machdeps() +print("Known machdeps: " + " ".join(machdeps)) +machdep_chosen = False +while not machdep_chosen: + machdep = input("Please enter the machdep [" + default_machdep + "]: ") + if not machdep: + machdep = default_machdep + machdep_chosen = True + else: + if not (machdep in machdeps): + yn = input("'{}' is not a standard machdep. Proceed anyway? [y/N]".format(machdep)) + if yn != "" and (yn[0] == "Y" or yn[0] == "y"): + machdep_chosen = True + else: + machdep_chosen = True + +def insert_line_after(lines, line_pattern, newline): + re_line = re.compile(line_pattern) + for i in range(0, len(lines)): + if re_line.search(lines[i]): + lines.insert(i+1, newline) + return lines + print("error: no lines found matching pattern: " + line_pattern) + sys.exit(1) + +def replace_line(lines, line_pattern, value): + re_line = re.compile(line_pattern) + for i in range(0, len(lines)): + if re_line.search(lines[i]): + lines[i] = value + return lines + print("error: no lines found matching pattern: " + line_pattern) + sys.exit(1) + +def remove_lines_between(lines, start_pattern, end_pattern): + re_start = re.compile(start_pattern) + re_end = re.compile(end_pattern) + first_to_remove = -1 + last_to_remove = -1 + for i in range(0, len(lines)): + if first_to_remove == -1 and re_start.search(lines[i]): + first_to_remove = i + elif re_end.search(lines[i]): + last_to_remove = i + break + if first_to_remove == -1: + print("error: could not find start pattern: " + start_pattern) + sys.exit(1) + elif last_to_remove == -1: + print("error: could not find end pattern: " + end_pattern) + sys.exit(1) + return (lines[:first_to_remove-1] if first_to_remove > 0 else []) + (lines[last_to_remove+1:] if last_to_remove < len(lines)-1 else []) + +with open(sharedir / "analysis-scripts" / "template.mk") as f: + lines = list(f) + lines = replace_line(lines, "^MAIN_TARGET :=", "MAIN_TARGET := {}\n".format(main)) + lines = remove_lines_between(lines, "Remove these lines.*main target", "^endif") + lines = replace_line(lines, "^\$\(MAIN_TARGET\).parse:", "$(MAIN_TARGET).parse: {}\n".format(sources)) + if json_compilation_database: + lines = insert_line_after(lines, "^FCFLAGS", " -json-compilation-database {} \\\n".format(json_compilation_database)) + lines = insert_line_after(lines, "^FCFLAGS", " -machdep {} \\\n".format(machdep)) + if add_main_stub: + check_path_exists("fc_stubs.c") + from shutil import copyfile + copyfile(sharedir / "analysis-scripts" / "fc_stubs.c", "fc_stubs.c") + print("Created stub for main function: fc_stubs.c") + +gnumakefile.write_text("".join(lines)) + +print("Template created: " + gnumakefile.name) + +if "PTESTS_TESTING" in os.environ: + print("Running ptests: cleaning up after tests...") + jcdb.unlink() + if add_main_stub: + Path("fc_stubs.c").unlink() diff --git a/tests/fc_script/main.c b/tests/fc_script/main.c index 176bf1c3e843ae5c5e9967c7c2607ec6006562d3..74d57214aa95bddef791ede03759c1a863e371da 100644 --- a/tests/fc_script/main.c +++ b/tests/fc_script/main.c @@ -1,6 +1,6 @@ /* run.config OPT: - EXECNOW: LOG GNUmakefile LOG make_template.res LOG make_template.err bin/frama-c-script make-template @PTEST_DIR@/result < @PTEST_DIR@/make_template.input > @PTEST_DIR@/result/make_template.res 2> @PTEST_DIR@/result/make_template.err + EXECNOW: LOG GNUmakefile LOG make_template.res LOG make_template.err PTESTS_TESTING= bin/frama-c-script make-template @PTEST_DIR@/result < @PTEST_DIR@/make_template.input > @PTEST_DIR@/result/make_template.res 2> @PTEST_DIR@/result/make_template.err EXECNOW: LOG list_files.res LOG list_files.err bin/frama-c-script list-files @PTEST_DIR@/list_files.json > @PTEST_DIR@/result/list_files.res 2> @PTEST_DIR@/result/list_files.err EXECNOW: LOG flamegraph.html LOG flamegraph.res LOG flamegraph.err NOGUI=1 bin/frama-c-script flamegraph @PTEST_DIR@/flamegraph.txt @PTEST_DIR@/result > @PTEST_DIR@/result/flamegraph.res 2> @PTEST_DIR@/result/flamegraph.err && rm -f @PTEST_DIR@/result/flamegraph.svg EXECNOW: LOG find_fun1.res LOG find_fun1.err bin/frama-c-script find-fun main2 @PTEST_DIR@ > @PTEST_DIR@/result/find_fun1.res 2> @PTEST_DIR@/result/find_fun1.err diff --git a/tests/fc_script/make_template.input b/tests/fc_script/make_template.input index 7d303e9f0445bd48f79321001776a02c34d473d3..62488cfcb339f48b9263e611e5e7422074143ddb 100644 --- a/tests/fc_script/make_template.input +++ b/tests/fc_script/make_template.input @@ -1,2 +1,7 @@ fc_script_main file1.c file*.c dir/more_files.c +y +y +invalid_machdep +n +x86_64 diff --git a/tests/fc_script/oracle/GNUmakefile b/tests/fc_script/oracle/GNUmakefile index 12cd496ed85d9e8a6db443df0b585e81836f2339..32e5f33f164497fed2b82f1f27117a01a2286360 100644 --- a/tests/fc_script/oracle/GNUmakefile +++ b/tests/fc_script/oracle/GNUmakefile @@ -29,6 +29,8 @@ CPPFLAGS += # (Optional) Frama-C general flags (parsing and kernel) FCFLAGS += \ + -machdep x86_64 \ + -json-compilation-database . \ -kernel-warn-key annot:missing-spec=abort \ -kernel-warn-key typing:implicit-function-declaration=abort \ @@ -48,7 +50,7 @@ all: $(TARGETS) # (MANDATORY) List of source files used by MAIN_TARGET. # If there is a JSON compilation database, # 'frama-c-script list-files' can help obtain it -$(MAIN_TARGET).parse: file1.c file*.c dir/more_files.c +$(MAIN_TARGET).parse: fc_stubs.c file1.c file*.c dir/more_files.c # The following targets are optional and provided for convenience only diff --git a/tests/fc_script/oracle/make_template.res b/tests/fc_script/oracle/make_template.res index 280e7bb79e5a815f6d82b1eabec5b59fae3bb21f..9f38d9c7e745cbea6fe458c9fc77cdcd03f48e1e 100644 --- a/tests/fc_script/oracle/make_template.res +++ b/tests/fc_script/oracle/make_template.res @@ -1 +1,6 @@ -Template created: tests/fc_script/result/GNUmakefile +Running ptests: setting up mock files... +Main target name: Source files separated by spaces (default if empty: *.c): 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_32]: 'invalid_machdep' is not a standard machdep. Proceed anyway? [y/N]Please enter the machdep [x86_32]: Created stub for main function: fc_stubs.c +Template created: GNUmakefile +Running ptests: cleaning up after tests...