summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml11
-rw-r--r--.gitmodules6
-rwxr-xr-xbin/analyze-archive.py5
-rwxr-xr-xbin/explore-kconfig.py98
-rwxr-xr-xbin/generate-dfa-benchmark.py3
l---------bin/versuchung1
m---------ext/kconfiglib0
m---------ext/versuchung0
-rw-r--r--lib/kconfig.py222
l---------lib/kconfiglib.py1
10 files changed, 345 insertions, 2 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f397fcd..c0b6c96 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -3,6 +3,15 @@ image: debian:bullseye
stages:
- test
+lint_python:
+ stage: test
+ script:
+ - apt-get update -qy
+ - apt-get install -y black
+ - black --check --diff bin
+ rules:
+ - if: '$CI_COMMIT_BRANCH == "master"'
+
run_tests:
stage: test
script:
@@ -21,7 +30,7 @@ run_tests:
- wget -qO test-data/20190815_111745_nRF24_no-rx.json https://lib.finalrewind.org/energy-models/20190815_111745_nRF24_no-rx.json
- wget -qO test-data/20190815_122531_nRF24_no-rx.json https://lib.finalrewind.org/energy-models/20190815_122531_nRF24_no-rx.json
- pytest-3 --cov=lib
- - python3-coverage html
+ - python3-coverage html -i
artifacts:
paths:
- htmlcov/
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..2baed33
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "kconfiglib"]
+ path = ext/kconfiglib
+ url = https://github.com/ulfalizer/Kconfiglib.git
+[submodule "versuchung"]
+ path = ext/versuchung
+ url = https://github.com/stettberger/versuchung.git
diff --git a/bin/analyze-archive.py b/bin/analyze-archive.py
index aa266ed..65d80b3 100755
--- a/bin/analyze-archive.py
+++ b/bin/analyze-archive.py
@@ -381,6 +381,11 @@ if __name__ == "__main__":
if "info" in opt:
print(" ".join(raw_data.filenames) + ":")
+ if raw_data.ptalog:
+ options = " --".join(
+ map(lambda kv: f"{kv[0]}={str(kv[1])}", raw_data.ptalog["opt"].items())
+ )
+ print(f" Options: --{options}")
if raw_data.version <= 1:
data_source = "MIMOSA"
elif raw_data.version == 2:
diff --git a/bin/explore-kconfig.py b/bin/explore-kconfig.py
new file mode 100755
index 0000000..4c08826
--- /dev/null
+++ b/bin/explore-kconfig.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+
+"""explore-kconfig - Obtain build attributes of configuration variants
+
+explore-kconfig obtains build attributes such as ROM or RAM usage of
+configuration variants for a given software project. It works on random
+random configurations (--random) or in the neighbourhood
+of existing configurations (--neighbourhood).
+
+Supported projects must be configurable via kconfig and provide a command which
+outputs a JSON dict of build attributes on stdout. Use
+--{clean,build,attribute}-command to configure explore-kconfig for a project.
+"""
+
+import argparse
+import logging
+import os
+import sys
+
+from dfatool import kconfig
+
+from versuchung.experiment import Experiment
+from versuchung.types import String, Bool, Integer
+from versuchung.files import File, Directory
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ formatter_class=argparse.RawDescriptionHelpFormatter, description=__doc__
+ )
+ parser.add_argument(
+ "--neighbourhood",
+ type=str,
+ help="Explore neighbourhood of provided .config file(s)",
+ )
+ parser.add_argument(
+ "--log-level",
+ default=logging.INFO,
+ type=lambda level: getattr(logging, level.upper()),
+ help="Set log level",
+ )
+ parser.add_argument(
+ "--random",
+ type=int,
+ help="Explore a number of random configurations (make randconfig)",
+ )
+ parser.add_argument(
+ "--clean-command", type=str, help="Clean command", default="make clean"
+ )
+ parser.add_argument(
+ "--build-command", type=str, help="Build command", default="make"
+ )
+ parser.add_argument(
+ "--attribute-command",
+ type=str,
+ help="Attribute extraction command",
+ default="make attributes",
+ )
+ parser.add_argument("project_root", type=str, help="Project root directory")
+
+ args = parser.parse_args()
+
+ if isinstance(args.log_level, int):
+ logging.basicConfig(level=args.log_level)
+ else:
+ print(f"Invalid log level. Setting log level to INFO.", file=sys.stderr)
+
+ kconf = kconfig.KConfig(args.project_root)
+
+ if args.clean_command:
+ kconf.clean_command = args.clean_command
+ if args.build_command:
+ kconf.build_command = args.build_command
+ if args.attribute_command:
+ kconf.attribute_command = args.attribute_command
+
+ if args.random:
+ for i in range(args.random):
+ logging.info(f"Running randconfig {i+1} of {args.random}")
+ kconf.run_randconfig()
+
+ if args.neighbourhood:
+ if os.path.isfile(args.neighbourhood):
+ kconf.run_exploration_from_file(args.neighbourhood)
+ elif os.path.isdir(args.neighbourhood):
+ for filename in os.listdir(args.neighbourhood):
+ config_filename = f"{args.neighbourhood}/{filename}"
+ logging.info(f"Exploring neighbourhood of {config_filename}")
+ kconf.run_exploration_from_file(config_filename)
+ else:
+ print(
+ f"--neighbourhod: Error: {args.neighbourhood} must be a file or directory, but is neither",
+ file=sys.stderr,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/bin/generate-dfa-benchmark.py b/bin/generate-dfa-benchmark.py
index e6c3001..64f8f73 100755
--- a/bin/generate-dfa-benchmark.py
+++ b/bin/generate-dfa-benchmark.py
@@ -593,7 +593,8 @@ if __name__ == "__main__":
if "codegen" in driver_definition and "flags" in driver_definition["codegen"]:
if run_flags is None:
run_flags = driver_definition["codegen"]["flags"]
- run_flags.extend(opt["run"].split())
+ if "run" in opt:
+ run_flags.extend(opt["run"].split())
runs = list(
pta.dfs(
diff --git a/bin/versuchung b/bin/versuchung
new file mode 120000
index 0000000..57b45a8
--- /dev/null
+++ b/bin/versuchung
@@ -0,0 +1 @@
+../ext/versuchung/src/versuchung \ No newline at end of file
diff --git a/ext/kconfiglib b/ext/kconfiglib
new file mode 160000
+Subproject 061e71f7d78cb057762d88de088055361863def
diff --git a/ext/versuchung b/ext/versuchung
new file mode 160000
+Subproject 849520ee1eed198f52094b63fa71d32c50a7d0d
diff --git a/lib/kconfig.py b/lib/kconfig.py
new file mode 100644
index 0000000..6ae947a
--- /dev/null
+++ b/lib/kconfig.py
@@ -0,0 +1,222 @@
+#!/usr/bin/env python3
+
+import kconfiglib
+import logging
+import re
+import shutil
+import subprocess
+
+from versuchung.experiment import Experiment
+from versuchung.types import String, Bool, Integer
+from versuchung.files import File, Directory
+
+logger = logging.getLogger(__name__)
+
+
+class AttributeExperiment(Experiment):
+ outputs = {
+ "config": File(".config"),
+ "attributes": File("attributes.json"),
+ "build_out": File("build.out"),
+ "build_err": File("build.err"),
+ }
+
+ def run(self):
+ build_command = self.build_command.value.split()
+ attr_command = self.attr_command.value.split()
+ shutil.copyfile(f"{self.project_root.path}/.config", self.config.path)
+ subprocess.check_call(
+ ["make", "clean"],
+ cwd=self.project_root.path,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+ try:
+ with open(self.build_out.path, "w") as out_fd, open(
+ self.build_err.path, "w"
+ ) as err_fd:
+ subprocess.check_call(
+ build_command,
+ cwd=self.project_root.path,
+ stdout=out_fd,
+ stderr=err_fd,
+ )
+ except subprocess.CalledProcessError:
+ logger.info("build error")
+ return
+ with open(self.attributes.path, "w") as attr_fd:
+ subprocess.check_call(
+ attr_command, cwd=self.project_root.path, stdout=attr_fd
+ )
+
+
+class RandomConfig(AttributeExperiment):
+ inputs = {
+ "randconfig_seed": String("FIXME"),
+ "kconfig_hash": String("FIXME"),
+ "project_root": Directory("/tmp"),
+ "project_version": String("FIXME"),
+ "clean_command": String("make clean"),
+ "build_command": String("make"),
+ "attr_command": String("make attributes"),
+ }
+
+
+class ExploreConfig(AttributeExperiment):
+ inputs = {
+ "config_hash": String("FIXME"),
+ "kconfig_hash": String("FIXME"),
+ "project_root": Directory("/tmp"),
+ "project_version": String("FIXME"),
+ "clean_command": String("make clean"),
+ "build_command": String("make"),
+ "attr_command": String("make attributes"),
+ }
+
+
+class KConfig:
+ def __init__(self, working_directory):
+ self.cwd = working_directory
+ self.clean_command = "make clean"
+ self.build_command = "make"
+ self.attribute_command = "make attributes"
+
+ def randconfig(self):
+ status = subprocess.run(
+ ["make", "randconfig"],
+ cwd=self.cwd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ universal_newlines=True,
+ )
+
+ # make randconfig occasionally generates illegal configurations, so a project may run randconfig more than once.
+ # Make sure to return the seed of the latest run (don't short-circuit).
+ seed = None
+ for line in status.stderr.split("\n"):
+ match = re.match("KCONFIG_SEED=(.*)", line)
+ if match:
+ seed = match.group(1)
+ if seed:
+ return seed
+ raise RuntimeError("KCONFIG_SEED not found")
+
+ def git_commit_id(self):
+ status = subprocess.run(
+ ["git", "rev-parse", "HEAD"],
+ cwd=self.cwd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ universal_newlines=True,
+ )
+ revision = status.stdout.strip()
+ return revision
+
+ def file_hash(self, config_file):
+ status = subprocess.run(
+ ["sha256sum", config_file],
+ cwd=self.cwd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ universal_newlines=True,
+ )
+ sha256sum = status.stdout.split()[0]
+ return sha256sum
+
+ def run_randconfig(self):
+ """Run a randomconfig experiment in the selected project. Results are written to the current working directory."""
+ experiment = RandomConfig()
+ experiment(
+ [
+ "--randconfig_seed",
+ self.randconfig(),
+ "--kconfig_hash",
+ self.file_hash(f"{self.cwd}/Kconfig"),
+ "--project_version",
+ self.git_commit_id(),
+ "--project_root",
+ self.cwd,
+ "--clean_command",
+ self.clean_command,
+ "--build_command",
+ self.build_command,
+ "--attr_command",
+ self.attribute_command,
+ ]
+ )
+
+ def config_is_functional(self, kconf):
+ for choice in kconf.choices:
+ if (
+ not choice.is_optional
+ and 2 in choice.assignable
+ and choice.selection is None
+ ):
+ return False
+ return True
+
+ def run_exploration_from_file(self, config_file):
+ kconfig_file = f"{self.cwd}/Kconfig"
+ kconf = kconfiglib.Kconfig(kconfig_file)
+ kconf.load_config(config_file)
+ symbols = list(kconf.syms.keys())
+
+ experiment = ExploreConfig()
+ shutil.copyfile(config_file, f"{self.cwd}/.config")
+ experiment(
+ [
+ "--config_hash",
+ self.file_hash(config_file),
+ "--kconfig_hash",
+ self.file_hash(kconfig_file),
+ "--project_version",
+ self.git_commit_id(),
+ "--project_root",
+ self.cwd,
+ "--clean_command",
+ self.clean_command,
+ "--build_command",
+ self.build_command,
+ "--attr_command",
+ self.attribute_command,
+ ]
+ )
+
+ for symbol in kconf.syms.values():
+ if kconfiglib.TYPE_TO_STR[symbol.type] != "bool":
+ continue
+ if symbol.tri_value == 0 and 2 in symbol.assignable:
+ logger.debug(f"Set {symbol.name} to y")
+ symbol.set_value(2)
+ elif symbol.tri_value == 2 and 0 in symbol.assignable:
+ logger.debug(f"Set {symbol.name} to n")
+ symbol.set_value(0)
+ else:
+ continue
+
+ if not self.config_is_functional(kconf):
+ logger.debug("Configuration is non-functional")
+ kconf.load_config(config_file)
+ continue
+
+ kconf.write_config(f"{self.cwd}/.config")
+ experiment = ExploreConfig()
+ experiment(
+ [
+ "--config_hash",
+ self.file_hash(f"{self.cwd}/.config"),
+ "--kconfig_hash",
+ self.file_hash(kconfig_file),
+ "--project_version",
+ self.git_commit_id(),
+ "--project_root",
+ self.cwd,
+ "--clean_command",
+ self.clean_command,
+ "--build_command",
+ self.build_command,
+ "--attr_command",
+ self.attribute_command,
+ ]
+ )
+ kconf.load_config(config_file)
diff --git a/lib/kconfiglib.py b/lib/kconfiglib.py
new file mode 120000
index 0000000..5b2f9ac
--- /dev/null
+++ b/lib/kconfiglib.py
@@ -0,0 +1 @@
+../ext/kconfiglib/kconfiglib.py \ No newline at end of file