fm-weck 1.2.1__py3-none-any.whl → 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. fm_weck/__init__.py +1 -1
  2. fm_weck/cli.py +104 -11
  3. fm_weck/config.py +87 -17
  4. fm_weck/engine.py +84 -21
  5. fm_weck/exceptions.py +9 -0
  6. fm_weck/image_mgr.py +13 -4
  7. fm_weck/resources/BenchExec-3.25-py3-none-any.whl +0 -0
  8. fm_weck/resources/BenchExec-3.25-py3-none-any.whl.license +3 -0
  9. fm_weck/resources/__init__.py +3 -1
  10. fm_weck/resources/fm_tools/2ls.yml +1 -1
  11. fm_weck/resources/fm_tools/brick.yml +1 -1
  12. fm_weck/resources/fm_tools/bubaak-split.yml +5 -0
  13. fm_weck/resources/fm_tools/bubaak.yml +1 -1
  14. fm_weck/resources/fm_tools/cbmc.yml +5 -0
  15. fm_weck/resources/fm_tools/coveritest.yml +12 -2
  16. fm_weck/resources/fm_tools/cpa-witness2test.yml +11 -2
  17. fm_weck/resources/fm_tools/cpachecker.yml +5 -0
  18. fm_weck/resources/fm_tools/cpv.yml +8 -1
  19. fm_weck/resources/fm_tools/cseq.yml +15 -5
  20. fm_weck/resources/fm_tools/dartagnan.yml +1 -1
  21. fm_weck/resources/fm_tools/deagle.yml +1 -1
  22. fm_weck/resources/fm_tools/ebf.yml +1 -1
  23. fm_weck/resources/fm_tools/esbmc-kind.yml +1 -1
  24. fm_weck/resources/fm_tools/frama-c-sv.yml +1 -1
  25. fm_weck/resources/fm_tools/fshell-witness2test.yml +10 -1
  26. fm_weck/resources/fm_tools/fusebmc-ia.yml +1 -1
  27. fm_weck/resources/fm_tools/fusebmc.yml +1 -1
  28. fm_weck/resources/fm_tools/gdart.yml +1 -1
  29. fm_weck/resources/fm_tools/goblint.yml +1 -1
  30. fm_weck/resources/fm_tools/gwit.yml +1 -1
  31. fm_weck/resources/fm_tools/infer.yml +13 -5
  32. fm_weck/resources/fm_tools/jayhorn.yml +1 -1
  33. fm_weck/resources/fm_tools/jbmc.yml +1 -1
  34. fm_weck/resources/fm_tools/key.yml +8 -0
  35. fm_weck/resources/fm_tools/korn.yml +1 -1
  36. fm_weck/resources/fm_tools/liv.yml +13 -1
  37. fm_weck/resources/fm_tools/metaval.yml +14 -1
  38. fm_weck/resources/fm_tools/mlb.yml +1 -1
  39. fm_weck/resources/fm_tools/mopsa.yml +1 -1
  40. fm_weck/resources/fm_tools/pesco.yml +1 -1
  41. fm_weck/resources/fm_tools/predatorhp.yml +1 -1
  42. fm_weck/resources/fm_tools/prtest.yml +1 -1
  43. fm_weck/resources/fm_tools/schema.yml +16 -0
  44. fm_weck/resources/fm_tools/symbiotic.yml +10 -2
  45. fm_weck/resources/fm_tools/testcov.yml +1 -1
  46. fm_weck/resources/fm_tools/theta.yml +1 -1
  47. fm_weck/resources/fm_tools/tracerx.yml +1 -1
  48. fm_weck/resources/fm_tools/uautomizer.yml +5 -1
  49. fm_weck/resources/fm_tools/ugemcutter.yml +1 -1
  50. fm_weck/resources/fm_tools/ukojak.yml +1 -1
  51. fm_weck/resources/fm_tools/utaipan.yml +1 -1
  52. fm_weck/resources/fm_tools/veriabs.yml +2 -2
  53. fm_weck/resources/fm_tools/veriabsl.yml +2 -2
  54. fm_weck/resources/fm_tools/verifuzz.yml +1 -1
  55. fm_weck/resources/run_with_overlay.sh +23 -5
  56. fm_weck/resources/runexec +16 -0
  57. fm_weck/runexec_mode.py +52 -0
  58. fm_weck/runexec_util.py +30 -0
  59. fm_weck/serve.py +2 -2
  60. {fm_weck-1.2.1.dist-info → fm_weck-1.4.0.dist-info}/METADATA +1 -1
  61. {fm_weck-1.2.1.dist-info → fm_weck-1.4.0.dist-info}/RECORD +63 -57
  62. {fm_weck-1.2.1.dist-info → fm_weck-1.4.0.dist-info}/WHEEL +1 -1
  63. {fm_weck-1.2.1.dist-info → fm_weck-1.4.0.dist-info}/entry_points.txt +0 -0
fm_weck/__init__.py CHANGED
@@ -8,4 +8,4 @@
8
8
  from .config import Config # noqa: F401
9
9
  from .image_mgr import ImageMgr # noqa: F401
10
10
 
11
- __version__ = "1.2.1"
11
+ __version__ = "1.4.0"
fm_weck/cli.py CHANGED
@@ -14,15 +14,29 @@ from functools import cache
14
14
  from pathlib import Path
15
15
  from typing import Any, Callable, Optional, Tuple, Union
16
16
 
17
- from fm_tools.benchexec_helper import DataModel
17
+ try:
18
+ from fm_tools.benchexec_helper import DataModel
19
+ except ImportError:
20
+ from enum import Enum
21
+
22
+ class DataModel(Enum):
23
+ """
24
+ Enum representing the data model of the tool.
25
+ """
26
+
27
+ LP64 = "LP64"
28
+ ILP32 = "ILP32"
29
+
30
+ def __str__(self):
31
+ return self.value
32
+
18
33
 
19
34
  from fm_weck import Config
20
35
  from fm_weck.config import _SEARCH_ORDER
21
36
  from fm_weck.resources import iter_fm_data, iter_properties
22
37
 
23
38
  from . import __version__
24
- from .engine import Engine, NoImageError
25
- from .serve import run_guided, run_manual, setup_fm_tool
39
+ from .exceptions import NoImageError
26
40
 
27
41
  logger = logging.getLogger(__name__)
28
42
 
@@ -84,12 +98,22 @@ def parse(raw_args: list[str]) -> Tuple[Callable[[], None], Namespace]:
84
98
  default=None,
85
99
  )
86
100
 
101
+ loglevels_lower = ["debug", "info", "warning", "error", "critical"]
102
+ loglevels = loglevels_lower + [level.upper() for level in loglevels_lower]
87
103
  parser.add_argument(
88
104
  "--loglevel",
89
- choices=["debug", "info", "warning", "error", "critical"],
105
+ choices=loglevels,
106
+ metavar="LEVEL",
107
+ action="store",
108
+ default=None,
109
+ help="Set the log level. Valid values are: " + ", ".join(loglevels_lower),
110
+ )
111
+
112
+ parser.add_argument(
113
+ "--logfile",
90
114
  action="store",
115
+ help="Path to the log file.",
91
116
  default=None,
92
- help="Set the log level.",
93
117
  )
94
118
 
95
119
  parser.add_argument(
@@ -100,6 +124,14 @@ def parse(raw_args: list[str]) -> Tuple[Callable[[], None], Namespace]:
100
124
  default=False,
101
125
  )
102
126
 
127
+ parser.add_argument(
128
+ "--dry-run",
129
+ action="store_true",
130
+ help="Just print the command that would be executed.",
131
+ required=False,
132
+ default=False,
133
+ )
134
+
103
135
  subparsers = parser.add_subparsers()
104
136
 
105
137
  run = subparsers.add_parser("run", aliases=["r"], help="Run a verifier inside a container.")
@@ -173,6 +205,42 @@ def parse(raw_args: list[str]) -> Tuple[Callable[[], None], Namespace]:
173
205
  add_tool_arg(install, nargs="+")
174
206
  install.set_defaults(main=main_install)
175
207
 
208
+ runexec = subparsers.add_parser("runexec", help="Run runexec on a command inside a container.")
209
+ runexec.add_argument(
210
+ "--image",
211
+ dest="use_image",
212
+ action="store",
213
+ default=None,
214
+ type=str,
215
+ help=(
216
+ "The image that shall be used for the container."
217
+ " The image is treated as 'full_container_image', i.e., fm-weck will not attempt to install any packages"
218
+ " inside of the image"
219
+ ),
220
+ )
221
+
222
+ runexec.add_argument(
223
+ "--benchexec-path",
224
+ action="store",
225
+ dest="benchexec_package",
226
+ type=Path,
227
+ help=("The path to the benchexec .whl or .egg file. If not given, fm-weck will use its own benchexec package."),
228
+ default=None,
229
+ )
230
+
231
+ # Arguments passed though to the container manager (i.e., docker or podman)
232
+ runexec.add_argument(
233
+ "--container-long-opt",
234
+ dest="container_long_opts",
235
+ help="Arguments passed as long options (prepending --) directly to the container manager "
236
+ "(e.g., docker or podman). Each usage passes additional arguments to the container manager.",
237
+ action="append",
238
+ nargs="+",
239
+ )
240
+
241
+ runexec.add_argument("argument_list", metavar="args", nargs="*", help="Arguments for runexec.")
242
+ runexec.set_defaults(main=main_runexec)
243
+
176
244
  def help_callback():
177
245
  parser.print_help()
178
246
 
@@ -230,14 +298,19 @@ def resolve_property(prop_name: str) -> Path:
230
298
  return property_choice_map()[prop_name]
231
299
 
232
300
 
233
- def set_log_level(loglevel: Optional[str], config: dict[str, Any]):
301
+ def set_log_options(loglevel: Optional[str], logfile: Optional[str], config: dict[str, Any]):
234
302
  level = "WARNING"
235
303
  level = loglevel.upper() if loglevel else config.get("logging", {}).get("level", level)
236
- logging.basicConfig(level=level)
304
+ if logfile:
305
+ logging.basicConfig(level=level, filename=logfile)
306
+ else:
307
+ logging.basicConfig(level=level)
237
308
  logging.getLogger("httpcore").setLevel("WARNING")
238
309
 
239
310
 
240
311
  def main_run(args: argparse.Namespace):
312
+ from .serve import run_guided
313
+
241
314
  if not args.TOOL:
242
315
  logger.error("No fm-tool given. Aborting...")
243
316
  return 1
@@ -265,7 +338,21 @@ def main_run(args: argparse.Namespace):
265
338
  )
266
339
 
267
340
 
341
+ def main_runexec(args: argparse.Namespace):
342
+ from .runexec_mode import run_runexec
343
+
344
+ run_runexec(
345
+ benchexec_package=args.benchexec_package,
346
+ use_image=args.use_image,
347
+ configuration=Config(),
348
+ extra_container_args=args.container_long_opts or [],
349
+ command=args.argument_list,
350
+ )
351
+
352
+
268
353
  def main_manual(args: argparse.Namespace):
354
+ from .serve import run_manual
355
+
269
356
  if not args.TOOL:
270
357
  logger.error("No fm-tool given. Aborting...")
271
358
  return 1
@@ -285,6 +372,8 @@ def main_manual(args: argparse.Namespace):
285
372
 
286
373
 
287
374
  def main_install(args: argparse.Namespace):
375
+ from .serve import setup_fm_tool
376
+
288
377
  for tool in args.TOOL:
289
378
  try:
290
379
  fm_data = resolve_tool(tool)
@@ -300,6 +389,8 @@ def main_install(args: argparse.Namespace):
300
389
 
301
390
 
302
391
  def main_shell(args: argparse.Namespace):
392
+ from .engine import Engine
393
+
303
394
  if not args.TOOL:
304
395
  engine = Engine.from_config(Config())
305
396
  else:
@@ -335,7 +426,7 @@ Please specify an image in the fm-tool yml file or add a configuration.
335
426
 
336
427
  To add a configuration you can do the following (on POSIX Terminals):
337
428
 
338
- printf '[defaults]\\nimage = "<your_image>"' > .weck
429
+ printf '[defaults]\\nimage = "<your_image>"' > .fm-weck
339
430
 
340
431
  Replace <your_image> with the image you want to use.
341
432
  """
@@ -350,7 +441,7 @@ To specify an image add
350
441
  [defaults]
351
442
  image = "your_image"
352
443
 
353
- to your .weck file.
444
+ to your .fm-weck file.
354
445
  """
355
446
 
356
447
  logger.error(text, tool, config)
@@ -359,7 +450,9 @@ to your .weck file.
359
450
  def cli(raw_args: list[str]):
360
451
  help_callback, args = parse(raw_args)
361
452
  configuration = Config().load(args.config)
362
- set_log_level(args.loglevel, configuration)
453
+ set_log_options(args.loglevel, args.logfile, configuration)
454
+ if args.dry_run:
455
+ Config().set_dry_run(True)
363
456
 
364
457
  if args.list:
365
458
  print("List of fm-tools callable by name:")
@@ -370,7 +463,7 @@ def cli(raw_args: list[str]):
370
463
  print(f" - {prop}")
371
464
  return
372
465
 
373
- if not hasattr(args, "TOOL"):
466
+ if not hasattr(args, "main"):
374
467
  return help_callback()
375
468
 
376
469
  try:
fm_weck/config.py CHANGED
@@ -5,16 +5,32 @@
5
5
  #
6
6
  # SPDX-License-Identifier: Apache-2.0
7
7
 
8
+ import importlib.resources as pkg_resources
9
+ import logging
8
10
  import os
9
11
  import shutil
12
+ import stat
13
+ import sys
10
14
  from functools import cache
11
15
  from pathlib import Path
12
16
  from typing import Any, Callable, Iterable, Optional, Tuple, TypeVar
13
17
 
14
- import yaml
15
- from fm_tools.fmdata import FmData
18
+ try:
19
+ from fm_tools.fmdata import FmData
20
+ except ImportError:
21
+
22
+ class FmData:
23
+ def __init__(self, data, version):
24
+ raise ImportError("fm_tools is not imported.")
25
+
26
+ def get_actor_name(self):
27
+ raise ImportError("fm_tools is not imported.")
16
28
 
17
- from fm_weck.resources import RUN_WITH_OVERLAY
29
+ def get_version(self):
30
+ raise ImportError("fm_tools is not imported.")
31
+
32
+
33
+ from fm_weck.resources import RUN_WITH_OVERLAY, RUNEXEC_SCRIPT
18
34
 
19
35
  try:
20
36
  import tomllib as toml
@@ -22,10 +38,10 @@ except ImportError:
22
38
  import tomli as toml
23
39
 
24
40
  _SEARCH_ORDER: tuple[Path, ...] = (
25
- Path.cwd() / ".weck",
26
- Path.home() / ".weck",
27
- Path.home() / ".config" / "weck",
28
- Path.home() / ".config" / "weck" / "config.toml",
41
+ Path.cwd() / ".fm-weck",
42
+ Path.home() / ".fm-weck",
43
+ Path.home() / ".config" / "fm-weck",
44
+ Path.home() / ".config" / "fm-weck" / "config.toml",
29
45
  )
30
46
  BASE_CONFIG = """
31
47
  [logging]
@@ -51,6 +67,7 @@ class Config(object):
51
67
  if cls._instance is None:
52
68
  cls._instance = super(Config, cls).__new__(cls)
53
69
  cls._instance._config = None
70
+ cls._instance._dry_run = False
54
71
  return cls._instance
55
72
 
56
73
  def load(self, config: Optional[Path] = None) -> dict[str, Any]:
@@ -88,6 +105,15 @@ class Config(object):
88
105
 
89
106
  return default
90
107
 
108
+ def set_dry_run(self, dry_run: bool) -> None:
109
+ self._dry_run = dry_run
110
+
111
+ def is_dry_run(self) -> bool:
112
+ return self._dry_run
113
+
114
+ def set_default_image(self, image: str) -> None:
115
+ self._config["defaults"]["image"] = image
116
+
91
117
  def defaults(self) -> dict[str, Any]:
92
118
  return self.get("defaults", {})
93
119
 
@@ -114,11 +140,11 @@ class Config(object):
114
140
  @property
115
141
  @_handle_relative_paths
116
142
  def cache_location(self) -> Path:
117
- cache = Path.home() / ".cache" / "weck_cache"
143
+ cache = Path.home() / ".cache" / "fm-weck_cache"
118
144
  xdg_cache_home = os.environ.get("XDG_CACHE_HOME")
119
145
 
120
146
  if xdg_cache_home:
121
- cache = Path(xdg_cache_home) / "weck_cache"
147
+ cache = Path(xdg_cache_home) / "fm-weck_cache"
122
148
 
123
149
  return Path(self.defaults().get("cache_location", cache.resolve()))
124
150
 
@@ -144,19 +170,63 @@ class Config(object):
144
170
  property_name = path.name
145
171
  return shelve / property_name
146
172
 
147
- def make_script_available(self) -> Path:
148
- script_dir = self.cache_location / ".scripts"
149
- run_script = script_dir / "run_with_overlay.sh"
150
- if run_script.exists() and run_script.is_file():
151
- return run_script
173
+ def get_shelve_path_for_benchexec(self) -> Path:
174
+ shelve = self.cache_location / ".lib" / "benchexec.whl"
175
+ shelve.parent.mkdir(parents=True, exist_ok=True)
176
+ return shelve
152
177
 
153
- script_dir.mkdir(parents=True, exist_ok=True)
154
- shutil.copy(RUN_WITH_OVERLAY, run_script)
155
- return run_script
178
+ @staticmethod
179
+ def _system_is_not_posix():
180
+ return not (sys.platform.startswith("linux") or sys.platform == "darwin")
181
+
182
+ def make_runexec_script_available(self) -> Path:
183
+ return self.make_script_available(RUNEXEC_SCRIPT)
184
+
185
+ def make_script_available(self, target_name: str = RUN_WITH_OVERLAY) -> Path:
186
+ script_dir = self.cache_location / ".scripts"
187
+ target = script_dir / target_name
188
+
189
+ if not (target.exists() and target.is_file()):
190
+ script_dir.mkdir(parents=True, exist_ok=True)
191
+
192
+ # Try to copy from package resources
193
+ try:
194
+ with pkg_resources.path("fm_weck.resources", target_name) as source_path:
195
+ shutil.copy(source_path, target)
196
+ except FileNotFoundError:
197
+ logging.error(f"Resource {target_name} not found in package.")
198
+ return None
199
+ else:
200
+ # Compare modification time if the file exists
201
+ with pkg_resources.path("fm_weck.resources", target_name) as source_path:
202
+ if source_path.stat().st_mtime > target.stat().st_mtime:
203
+ shutil.copy(source_path, target)
204
+ else:
205
+ logging.debug(f"Using existing {target_name} script")
206
+ return target
207
+
208
+ if Config._system_is_not_posix():
209
+ return target
210
+
211
+ try:
212
+ # Get the current file permissions
213
+ current_permissions = os.stat(target).st_mode
214
+
215
+ # Add the executable bit for the owner, group, and others
216
+ os.chmod(target, current_permissions | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
217
+ except OSError as e:
218
+ logging.error(
219
+ f"Failed to set executable bit: {e}. "
220
+ "This may lead to permission errors when running the script in the container."
221
+ )
222
+
223
+ return target
156
224
 
157
225
 
158
226
  @cache
159
227
  def parse_fm_data(fm_data: Path, version: Optional[str]) -> FmData:
228
+ import yaml
229
+
160
230
  if not fm_data.exists() or not fm_data.is_file():
161
231
  raise FileNotFoundError(f"fm data file {fm_data} does not exist")
162
232
 
fm_weck/engine.py CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  import logging
9
9
  import shutil
10
+ import signal
10
11
  import subprocess
11
12
  from abc import ABC, abstractmethod
12
13
  from functools import cached_property, singledispatchmethod
@@ -14,38 +15,55 @@ from pathlib import Path
14
15
  from tempfile import mkdtemp
15
16
  from typing import List, Optional, Union
16
17
 
17
- from fm_tools.fmdata import FmData, FmImageConfig
18
+ try:
19
+ from fm_tools.fmdata import FmData, FmImageConfig
20
+ except ImportError:
21
+ # Mock the FmData and FmImageConfig class for type checking
22
+ class FmData:
23
+ def get_images(self):
24
+ pass
25
+
26
+ class FmImageConfig:
27
+ pass
28
+
18
29
 
19
30
  from fm_weck.config import Config, parse_fm_data
31
+ from fm_weck.exceptions import NoImageError
20
32
  from fm_weck.image_mgr import ImageMgr
21
33
 
22
34
  logger = logging.getLogger(__name__)
23
35
 
24
36
 
25
- class NoImageError(Exception):
26
- pass
37
+ CWD_MOUNT_LOCATION = "/home/cwd"
38
+ CACHE_MOUNT_LOCATION = "/home/fm-weck_cache"
39
+ OUTPUT_MOUNT_LOCATION = "/home/output"
40
+
41
+ RESERVED_LOCATIONS = frozenset([CACHE_MOUNT_LOCATION, CWD_MOUNT_LOCATION, OUTPUT_MOUNT_LOCATION])
27
42
 
28
43
 
29
44
  class Engine(ABC):
30
45
  interactive: bool = False
31
46
  add_benchexec_capabilities: bool = False
32
47
  image: Optional[str] = None
48
+ dry_run: bool = False
33
49
 
34
50
  def __init__(self, image: Union[str, FmImageConfig]):
51
+ self._tmp_output_dir = Path(mkdtemp("fm_weck_output")).resolve()
35
52
  self.image = self._initialize_image(image)
36
53
  self.extra_args = {}
37
54
  self._engine = None
38
- self._tmp_output_dir = Path(mkdtemp("fm_weck_output")).resolve()
39
55
 
40
56
  self.output_dir = Path.cwd() / "output"
41
57
  self.log_file = None
42
58
 
59
+ self.env = {}
60
+
43
61
  def __del__(self):
44
62
  if self._tmp_output_dir.exists():
45
63
  shutil.rmtree(self._tmp_output_dir)
46
64
 
47
65
  def get_workdir(self):
48
- return Path("/home/cwd")
66
+ return Path(CWD_MOUNT_LOCATION)
49
67
 
50
68
  def set_output_log(self, output_log: Path):
51
69
  self.log_file = output_log
@@ -59,6 +77,24 @@ class Engine(ABC):
59
77
  f"{source}:{target}",
60
78
  ]
61
79
 
80
+ def add_container_long_opt(self, arg: list[str]):
81
+ """
82
+ Add a long option to the container command.
83
+ If the first element of the list does not start with "--", it will be prepended.
84
+ Example:
85
+ add_container_long_opt(["--option", "value"]) -> --option value
86
+ add_container_long_opt(["option", "value"]) -> --option value
87
+ """
88
+
89
+ if not arg:
90
+ raise ValueError("Argument must not be empty.")
91
+
92
+ base = arg[0]
93
+ if not base.startswith("--"):
94
+ base = f"--{base}"
95
+
96
+ self.extra_args["container_args"] = self.extra_args.get("container_args", []) + [base] + arg[1:]
97
+
62
98
  @abstractmethod
63
99
  def benchexec_capabilities(self):
64
100
  raise NotImplementedError
@@ -69,6 +105,9 @@ class Engine(ABC):
69
105
  def interactive_command(self):
70
106
  return ["-it"]
71
107
 
108
+ def add_environment(self):
109
+ return sum([["-e", f"{key}={value}"] for key, value in self.env.items()], [])
110
+
72
111
  def setup_command(self):
73
112
  return [
74
113
  "--entrypoint",
@@ -76,11 +115,11 @@ class Engine(ABC):
76
115
  "--cap-add",
77
116
  "SYS_ADMIN",
78
117
  "-v",
79
- f"{Path.cwd().absolute()}:/home/cwd",
118
+ f"{Path.cwd().absolute()}:{CWD_MOUNT_LOCATION}",
80
119
  "-v",
81
- f"{Config().cache_location}:/home/weck_cache",
120
+ f"{Config().cache_location}:{CACHE_MOUNT_LOCATION}",
82
121
  "-v",
83
- f"{self._tmp_output_dir}:/home/output",
122
+ f"{self._tmp_output_dir}:{OUTPUT_MOUNT_LOCATION}",
84
123
  "--workdir",
85
124
  str(self.get_workdir()),
86
125
  "--rm",
@@ -101,6 +140,7 @@ class Engine(ABC):
101
140
  base += self.benchexec_capabilities()
102
141
 
103
142
  base += self.setup_command()
143
+ base += self.add_environment()
104
144
 
105
145
  if self.interactive:
106
146
  base += self.interactive_command()
@@ -127,7 +167,7 @@ class Engine(ABC):
127
167
  return self.get_workdir() / relative
128
168
  elif p.is_relative_to(Config().cache_location):
129
169
  relative = p.relative_to(Config().cache_location)
130
- return Path("/home/weck_cache") / relative
170
+ return Path(CACHE_MOUNT_LOCATION) / relative
131
171
  else:
132
172
  return p
133
173
  mapped = _map_path(Path(p))
@@ -156,7 +196,7 @@ class Engine(ABC):
156
196
 
157
197
  @staticmethod
158
198
  def _base_engine_class(config: Config):
159
- engine = config.defaults().get("engine", "").lower()
199
+ engine = config.defaults().get("engine", "podman").lower()
160
200
 
161
201
  if engine == "docker":
162
202
  return Docker
@@ -204,6 +244,9 @@ class Engine(ABC):
204
244
  continue
205
245
  engine.mount(src, target)
206
246
 
247
+ if config.is_dry_run():
248
+ engine.dry_run = True
249
+
207
250
  return engine
208
251
 
209
252
  @abstractmethod
@@ -247,22 +290,39 @@ class Engine(ABC):
247
290
  return tag
248
291
 
249
292
  def _run_process(self, command: tuple[str, ...] | list[str]):
293
+ process = None # To make sure process is defined if a signal is caught early
294
+
295
+ def terminate_process_group(signal_received, frame):
296
+ if process:
297
+ logging.info("Received signal %s. Terminating container process.", signal_received)
298
+ process.send_signal(signal.SIGTERM)
299
+
300
+ # Register signal handler
301
+ signal.signal(signal.SIGINT, terminate_process_group)
302
+ signal.signal(signal.SIGTERM, terminate_process_group)
303
+
304
+ logger.debug("\n\nRunning command:\n%s\n\n", " ".join(map(str, command)))
250
305
  if self.log_file is None:
251
306
  process = subprocess.Popen(command)
252
- process.wait()
253
- return
307
+ else:
308
+ self.log_file.parent.mkdir(parents=True, exist_ok=True)
309
+ with self.log_file.open("wb") as f:
310
+ process = subprocess.Popen(command, stdout=f, stderr=f)
254
311
 
255
- self.log_file.parent.mkdir(parents=True, exist_ok=True)
256
- with self.log_file.open("wb") as f:
257
- process = subprocess.Popen(command, stdout=f, stderr=f)
258
- process.wait()
312
+ assert process is not None, "Process should be defined at this point."
313
+ process.wait()
259
314
 
260
315
  def run(self, *command: str) -> None:
261
316
  if self.image is None:
262
317
  raise NoImageError("No image set for engine.")
263
318
 
264
319
  command = self.assemble_command(command)
265
- logger.info("Running: %s", command)
320
+ logger.debug("Running: %s", command)
321
+ if self.dry_run:
322
+ print("Command to be executed:")
323
+ print(" ".join(map(str, command)))
324
+ return
325
+
266
326
  self._run_process(command)
267
327
  self._move_output()
268
328
 
@@ -284,12 +344,15 @@ class Podman(Engine):
284
344
  return [
285
345
  "--annotation",
286
346
  "run.oci.keep_original_groups=1",
347
+ # "--cgroups=split",
348
+ "--security-opt",
349
+ "unmask=/sys/fs/cgroup",
287
350
  "--security-opt",
288
351
  "unmask=/proc/*",
289
352
  "--security-opt",
290
353
  "seccomp=unconfined",
291
354
  "-v",
292
- "/sys/fs/cgroup:/sys/fs/cgroup",
355
+ "/sys/fs/cgroup:/sys/fs/cgroup:rw",
293
356
  ]
294
357
 
295
358
 
@@ -336,11 +399,11 @@ class Docker(Engine):
336
399
  "--cap-add",
337
400
  "SYS_ADMIN",
338
401
  "-v",
339
- f"{Path.cwd().absolute()}:/home/cwd",
402
+ f"{Path.cwd().absolute()}:{CWD_MOUNT_LOCATION}",
340
403
  "-v",
341
- f"{Config().cache_location}:/home/weck_cache",
404
+ f"{Config().cache_location}:{CACHE_MOUNT_LOCATION}",
342
405
  "-v",
343
- f"{self._tmp_output_dir}:/home/output",
406
+ f"{self._tmp_output_dir}:{OUTPUT_MOUNT_LOCATION}",
344
407
  "--workdir",
345
408
  str(self.get_workdir()),
346
409
  "--rm",
fm_weck/exceptions.py ADDED
@@ -0,0 +1,9 @@
1
+ # This file is part of fm-weck: executing fm-tools in containerized environments.
2
+ # https://gitlab.com/sosy-lab/software/fm-weck
3
+ #
4
+ # SPDX-FileCopyrightText: 2024 Dirk Beyer <https://www.sosy-lab.org>
5
+ #
6
+ # SPDX-License-Identifier: Apache-2.0
7
+
8
+ class NoImageError(Exception):
9
+ pass
fm_weck/image_mgr.py CHANGED
@@ -9,10 +9,16 @@ import logging
9
9
  from pathlib import Path
10
10
  from typing import TYPE_CHECKING
11
11
 
12
- from fm_tools.fmdata import FmImageConfig
13
- from yaspin import yaspin
12
+ try:
13
+ from fm_tools.fmdata import FmImageConfig
14
+ except ImportError:
14
15
 
15
- from fm_weck import Config
16
+ class FmImageConfig:
17
+ def __init__(self, full_images, base_images, required_packages):
18
+ raise ImportError("fm_tools is not imported.")
19
+
20
+
21
+ from fm_weck.exceptions import NoImageError
16
22
 
17
23
  if TYPE_CHECKING:
18
24
  from fm_weck.engine import Engine
@@ -30,7 +36,6 @@ class ImageMgr(object):
30
36
  def __new__(cls):
31
37
  if cls._instance is None:
32
38
  cls._instance = super(ImageMgr, cls).__new__(cls)
33
- cls._instance.image_db = Config().get("images", {}).get("database", None) or ":memory:"
34
39
  return cls._instance
35
40
 
36
41
  def prepare_image(self, engine: "Engine", image: FmImageConfig) -> str:
@@ -40,12 +45,16 @@ class ImageMgr(object):
40
45
  if image.base_images and not image.required_packages:
41
46
  return image.base_images[0]
42
47
 
48
+ if not image.base_images:
49
+ raise NoImageError("No base image specified")
50
+
43
51
  logging.info(
44
52
  "Building image from from base image %s with packages %s", image.base_images[0], image.required_packages
45
53
  )
46
54
  image_cmd = engine.image_from(CONTAINERFILE)
47
55
  image_cmd.base_image(image.base_images[0])
48
56
  image_cmd.packages(image.required_packages)
57
+ from yaspin import yaspin
49
58
 
50
59
  with yaspin(text="Building image", color="cyan") as spinner:
51
60
  tag = image_cmd.build()
@@ -0,0 +1,3 @@
1
+ SPDX-FileCopyrightText: The BenchExec authors <https://github.com/sosy-lab/benchexec>
2
+
3
+ SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause AND CC-BY-4.0 AND MIT AND ISC AND LicenseRef-BSD-3-Clause-CMU
@@ -15,7 +15,9 @@ CONTAINERFILE = resource_dir / "Containerfile"
15
15
  # to the wheel file under fm_weck/resources/fm_tools
16
16
  FM_DATA_LOCATION = resource_dir / "fm_tools"
17
17
  PROPERTY_LOCATION = resource_dir / "properties"
18
- RUN_WITH_OVERLAY = resource_dir / "run_with_overlay.sh"
18
+ RUN_WITH_OVERLAY = "run_with_overlay.sh"
19
+ BENCHEXEC_WHL = resource_dir / "BenchExec-3.25-py3-none-any.whl"
20
+ RUNEXEC_SCRIPT = "runexec"
19
21
 
20
22
 
21
23
  def iter_fm_data():