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.
- fm_weck/__init__.py +1 -1
- fm_weck/cli.py +104 -11
- fm_weck/config.py +87 -17
- fm_weck/engine.py +84 -21
- fm_weck/exceptions.py +9 -0
- fm_weck/image_mgr.py +13 -4
- fm_weck/resources/BenchExec-3.25-py3-none-any.whl +0 -0
- fm_weck/resources/BenchExec-3.25-py3-none-any.whl.license +3 -0
- fm_weck/resources/__init__.py +3 -1
- fm_weck/resources/fm_tools/2ls.yml +1 -1
- fm_weck/resources/fm_tools/brick.yml +1 -1
- fm_weck/resources/fm_tools/bubaak-split.yml +5 -0
- fm_weck/resources/fm_tools/bubaak.yml +1 -1
- fm_weck/resources/fm_tools/cbmc.yml +5 -0
- fm_weck/resources/fm_tools/coveritest.yml +12 -2
- fm_weck/resources/fm_tools/cpa-witness2test.yml +11 -2
- fm_weck/resources/fm_tools/cpachecker.yml +5 -0
- fm_weck/resources/fm_tools/cpv.yml +8 -1
- fm_weck/resources/fm_tools/cseq.yml +15 -5
- fm_weck/resources/fm_tools/dartagnan.yml +1 -1
- fm_weck/resources/fm_tools/deagle.yml +1 -1
- fm_weck/resources/fm_tools/ebf.yml +1 -1
- fm_weck/resources/fm_tools/esbmc-kind.yml +1 -1
- fm_weck/resources/fm_tools/frama-c-sv.yml +1 -1
- fm_weck/resources/fm_tools/fshell-witness2test.yml +10 -1
- fm_weck/resources/fm_tools/fusebmc-ia.yml +1 -1
- fm_weck/resources/fm_tools/fusebmc.yml +1 -1
- fm_weck/resources/fm_tools/gdart.yml +1 -1
- fm_weck/resources/fm_tools/goblint.yml +1 -1
- fm_weck/resources/fm_tools/gwit.yml +1 -1
- fm_weck/resources/fm_tools/infer.yml +13 -5
- fm_weck/resources/fm_tools/jayhorn.yml +1 -1
- fm_weck/resources/fm_tools/jbmc.yml +1 -1
- fm_weck/resources/fm_tools/key.yml +8 -0
- fm_weck/resources/fm_tools/korn.yml +1 -1
- fm_weck/resources/fm_tools/liv.yml +13 -1
- fm_weck/resources/fm_tools/metaval.yml +14 -1
- fm_weck/resources/fm_tools/mlb.yml +1 -1
- fm_weck/resources/fm_tools/mopsa.yml +1 -1
- fm_weck/resources/fm_tools/pesco.yml +1 -1
- fm_weck/resources/fm_tools/predatorhp.yml +1 -1
- fm_weck/resources/fm_tools/prtest.yml +1 -1
- fm_weck/resources/fm_tools/schema.yml +16 -0
- fm_weck/resources/fm_tools/symbiotic.yml +10 -2
- fm_weck/resources/fm_tools/testcov.yml +1 -1
- fm_weck/resources/fm_tools/theta.yml +1 -1
- fm_weck/resources/fm_tools/tracerx.yml +1 -1
- fm_weck/resources/fm_tools/uautomizer.yml +5 -1
- fm_weck/resources/fm_tools/ugemcutter.yml +1 -1
- fm_weck/resources/fm_tools/ukojak.yml +1 -1
- fm_weck/resources/fm_tools/utaipan.yml +1 -1
- fm_weck/resources/fm_tools/veriabs.yml +2 -2
- fm_weck/resources/fm_tools/veriabsl.yml +2 -2
- fm_weck/resources/fm_tools/verifuzz.yml +1 -1
- fm_weck/resources/run_with_overlay.sh +23 -5
- fm_weck/resources/runexec +16 -0
- fm_weck/runexec_mode.py +52 -0
- fm_weck/runexec_util.py +30 -0
- fm_weck/serve.py +2 -2
- {fm_weck-1.2.1.dist-info → fm_weck-1.4.0.dist-info}/METADATA +1 -1
- {fm_weck-1.2.1.dist-info → fm_weck-1.4.0.dist-info}/RECORD +63 -57
- {fm_weck-1.2.1.dist-info → fm_weck-1.4.0.dist-info}/WHEEL +1 -1
- {fm_weck-1.2.1.dist-info → fm_weck-1.4.0.dist-info}/entry_points.txt +0 -0
fm_weck/__init__.py
CHANGED
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
|
-
|
|
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 .
|
|
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=
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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, "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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(
|
|
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()}
|
|
118
|
+
f"{Path.cwd().absolute()}:{CWD_MOUNT_LOCATION}",
|
|
80
119
|
"-v",
|
|
81
|
-
f"{Config().cache_location}
|
|
120
|
+
f"{Config().cache_location}:{CACHE_MOUNT_LOCATION}",
|
|
82
121
|
"-v",
|
|
83
|
-
f"{self._tmp_output_dir}
|
|
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(
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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.
|
|
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()}
|
|
402
|
+
f"{Path.cwd().absolute()}:{CWD_MOUNT_LOCATION}",
|
|
340
403
|
"-v",
|
|
341
|
-
f"{Config().cache_location}
|
|
404
|
+
f"{Config().cache_location}:{CACHE_MOUNT_LOCATION}",
|
|
342
405
|
"-v",
|
|
343
|
-
f"{self._tmp_output_dir}
|
|
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
|
-
|
|
13
|
-
from
|
|
12
|
+
try:
|
|
13
|
+
from fm_tools.fmdata import FmImageConfig
|
|
14
|
+
except ImportError:
|
|
14
15
|
|
|
15
|
-
|
|
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()
|
|
Binary file
|
fm_weck/resources/__init__.py
CHANGED
|
@@ -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 =
|
|
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():
|