fm-weck 1.1.1__py3-none-any.whl → 1.1.3__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 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.1.1"
11
+ __version__ = "1.1.3"
fm_weck/cli.py CHANGED
@@ -24,6 +24,8 @@ from . import __version__
24
24
  from .engine import Engine, NoImageError
25
25
  from .serve import run_guided, run_manual, setup_fm_tool
26
26
 
27
+ logger = logging.getLogger(__name__)
28
+
27
29
 
28
30
  @dataclass
29
31
  class ToolQualifier:
@@ -232,22 +234,23 @@ def set_log_level(loglevel: Optional[str], config: dict[str, Any]):
232
234
  level = "WARNING"
233
235
  level = loglevel.upper() if loglevel else config.get("logging", {}).get("level", level)
234
236
  logging.basicConfig(level=level)
237
+ logging.getLogger("httpcore").setLevel("WARNING")
235
238
 
236
239
 
237
240
  def main_run(args: argparse.Namespace):
238
241
  if not args.TOOL:
239
- logging.error("No fm-tool given. Aborting...")
242
+ logger.error("No fm-tool given. Aborting...")
240
243
  return 1
241
244
  try:
242
245
  fm_data = resolve_tool(args.TOOL)
243
246
  except KeyError:
244
- logging.error("Unknown tool %s", args.TOOL)
247
+ logger.error("Unknown tool %s", args.TOOL)
245
248
  return 1
246
249
 
247
250
  try:
248
251
  property_path = resolve_property(args.property) if args.property else None
249
252
  except KeyError:
250
- logging.error("Unknown property %s", args.property)
253
+ logger.error("Unknown property %s", args.property)
251
254
  return 1
252
255
 
253
256
  run_guided(
@@ -264,12 +267,12 @@ def main_run(args: argparse.Namespace):
264
267
 
265
268
  def main_manual(args: argparse.Namespace):
266
269
  if not args.TOOL:
267
- logging.error("No fm-tool given. Aborting...")
270
+ logger.error("No fm-tool given. Aborting...")
268
271
  return 1
269
272
  try:
270
273
  fm_data = resolve_tool(args.TOOL)
271
274
  except KeyError:
272
- logging.error("Unknown tool %s", args.TOOL)
275
+ logger.error("Unknown tool %s", args.TOOL)
273
276
  return 1
274
277
 
275
278
  run_manual(
@@ -286,7 +289,7 @@ def main_install(args: argparse.Namespace):
286
289
  try:
287
290
  fm_data = resolve_tool(tool)
288
291
  except KeyError:
289
- logging.error("Unknown tool %s. Skipping installation...", tool)
292
+ logger.error("Unknown tool %s. Skipping installation...", tool)
290
293
  continue
291
294
 
292
295
  setup_fm_tool(
@@ -303,7 +306,7 @@ def main_shell(args: argparse.Namespace):
303
306
  try:
304
307
  fm_data = resolve_tool(args.TOOL)
305
308
  except KeyError:
306
- logging.error("Unknown tool %s", args.fm_data)
309
+ logger.error("Unknown tool %s", args.fm_data)
307
310
  return 1
308
311
  engine = Engine.from_config(fm_data, args.TOOL.version, Config())
309
312
  engine.interactive = True
@@ -336,7 +339,7 @@ printf '[defaults]\\nimage = "<your_image>"' > .weck
336
339
 
337
340
  Replace <your_image> with the image you want to use.
338
341
  """
339
- logging.error(text)
342
+ logger.error(text)
340
343
  return
341
344
 
342
345
  text = """
@@ -350,7 +353,7 @@ image = "your_image"
350
353
  to your .weck file.
351
354
  """
352
355
 
353
- logging.error(text, tool, config)
356
+ logger.error(text, tool, config)
354
357
 
355
358
 
356
359
  def cli(raw_args: list[str]):
fm_weck/config.py CHANGED
@@ -4,11 +4,12 @@
4
4
  # SPDX-FileCopyrightText: 2024 Dirk Beyer <https://www.sosy-lab.org>
5
5
  #
6
6
  # SPDX-License-Identifier: Apache-2.0
7
+
7
8
  import os
8
9
  import shutil
9
10
  from functools import cache
10
11
  from pathlib import Path
11
- from typing import Any, Optional
12
+ from typing import Any, Callable, Iterable, Optional, Tuple, TypeVar
12
13
 
13
14
  import yaml
14
15
  from fm_tools.fmdata import FmData
@@ -31,8 +32,11 @@ BASE_CONFIG = """
31
32
  level = "INFO"
32
33
 
33
34
  [defaults]
35
+ engine = "podman"
34
36
  """
35
37
 
38
+ _T = TypeVar("_T")
39
+
36
40
 
37
41
  class Config(object):
38
42
  """
@@ -41,6 +45,7 @@ class Config(object):
41
45
 
42
46
  _instance = None
43
47
  _config_source = None
48
+ _source_path = None
44
49
 
45
50
  def __new__(cls):
46
51
  if cls._instance is None:
@@ -58,6 +63,7 @@ class Config(object):
58
63
 
59
64
  with config.open("rb") as f:
60
65
  self._config = toml.load(f)
66
+ self._config_source = config.resolve()
61
67
  return self._config
62
68
 
63
69
  for path in _SEARCH_ORDER:
@@ -76,7 +82,7 @@ class Config(object):
76
82
  def __getitem__(self, key: str) -> Any:
77
83
  return self._config[key]
78
84
 
79
- def get(self, key: str, default: Any = None) -> Any:
85
+ def get(self, key: str, default: _T = None) -> _T:
80
86
  if self._config is not None:
81
87
  return self._config.get(key, default)
82
88
 
@@ -88,7 +94,25 @@ class Config(object):
88
94
  def from_defaults_or_none(self, key: str) -> Any:
89
95
  return self.defaults().get(key, None)
90
96
 
97
+ @staticmethod
98
+ def _handle_relative_paths(fn: Callable[[Any], Path]) -> Callable[[Any], Path]:
99
+ def wrapper(self, *args, **kwargs) -> Path:
100
+ """Makes sure relative Paths in the config are relative to the config file."""
101
+
102
+ path = fn(self, *args, **kwargs)
103
+
104
+ if not self._config_source:
105
+ return path
106
+
107
+ if path.is_absolute():
108
+ return path
109
+
110
+ return (self._config_source.parent / path).resolve()
111
+
112
+ return wrapper
113
+
91
114
  @property
115
+ @_handle_relative_paths
92
116
  def cache_location(self) -> Path:
93
117
  cache = Path.home() / ".cache" / "weck_cache"
94
118
  xdg_cache_home = os.environ.get("XDG_CACHE_HOME")
@@ -96,8 +120,16 @@ class Config(object):
96
120
  if xdg_cache_home:
97
121
  cache = Path(xdg_cache_home) / "weck_cache"
98
122
 
99
- return Path(self.defaults().get("cache_location", cache))
100
-
123
+ return Path(self.defaults().get("cache_location", cache.resolve()))
124
+
125
+ @_handle_relative_paths
126
+ def as_absolute_path(self, path: Path) -> Path:
127
+ return path
128
+
129
+ def mounts(self) -> Iterable[Tuple[Path, Path]]:
130
+ for local, container in self.get("mount", {}).items():
131
+ yield self.as_absolute_path(Path(local)), Path(container)
132
+
101
133
  def get_checksum_db(self) -> Path:
102
134
  return self.cache_location / ".checksums.dbm"
103
135
 
fm_weck/engine.py CHANGED
@@ -7,90 +7,217 @@
7
7
 
8
8
  import logging
9
9
  import subprocess
10
- import uuid
11
10
  from abc import ABC, abstractmethod
12
- from functools import singledispatchmethod
11
+ from functools import cached_property, singledispatchmethod
13
12
  from pathlib import Path
14
- from typing import Optional, Union
13
+ from typing import List, Optional, Union
15
14
 
16
15
  from fm_tools.fmdata import FmData, FmImageConfig
17
16
 
18
17
  from fm_weck.config import Config, parse_fm_data
19
18
  from fm_weck.image_mgr import ImageMgr
20
19
 
20
+ logger = logging.getLogger(__name__)
21
+
21
22
 
22
23
  class NoImageError(Exception):
23
24
  pass
24
25
 
26
+
25
27
  class Engine(ABC):
26
28
  interactive: bool = False
27
29
  add_benchexec_capabilities: bool = False
28
30
  image: Optional[str] = None
29
31
 
30
- @abstractmethod
31
- def assemble_command(self, command: tuple[str, ...]) -> list[str]:
32
- raise NotImplementedError
32
+ def __init__(self, image: Union[str, FmImageConfig]):
33
+ self.image = self._initialize_image(image)
34
+ self.extra_args = {}
35
+ self._engine = None
36
+
37
+ def get_workdir(self):
38
+ return Path("/home/cwd")
39
+
40
+ def mount(self, source: str, target: str):
41
+ self.extra_args["mounts"] = self.extra_args.get("mounts", []) + [
42
+ "-v",
43
+ f"{source}:{target}",
44
+ ]
33
45
 
34
46
  @abstractmethod
35
- def mount(self, src: str, target: str):
47
+ def benchexec_capabilities(self):
36
48
  raise NotImplementedError
37
49
 
50
+ def base_command(self):
51
+ return [self._engine, "run"]
52
+
53
+ def interactive_command(self):
54
+ return ["-it"]
55
+
56
+ def setup_command(self):
57
+ return [
58
+ "--entrypoint",
59
+ '[""]',
60
+ "--cap-add",
61
+ "SYS_ADMIN",
62
+ "-v",
63
+ f"{Path.cwd().absolute()}:/home/cwd",
64
+ "-v",
65
+ f"{Config().cache_location}:/home/weck_cache",
66
+ "--workdir",
67
+ str(self.get_workdir()),
68
+ "--rm",
69
+ ]
70
+
71
+ def assemble_command(self, command: tuple[str, ...]) -> list[str]:
72
+ base = self.base_command()
73
+ if self.add_benchexec_capabilities:
74
+ base += self.benchexec_capabilities()
75
+
76
+ base += self.setup_command()
77
+
78
+ if self.interactive:
79
+ base += self.interactive_command()
80
+
81
+ for value in self.extra_args.values():
82
+ if isinstance(value, list) and not isinstance(value, str):
83
+ base += value
84
+ else:
85
+ base.append(value)
86
+
87
+ _command = self._prep_command(command)
88
+ return base + [self.image, *_command]
89
+
90
+ def _prep_command(self, command: tuple[str, ...]) -> tuple[str, ...]:
91
+ """We want to map absolute paths of the current working directory to the
92
+ working directory of the container."""
93
+
94
+ def _map_path(p: Union[str, Path]) -> Union[str, Path]:
95
+ if isinstance(p, Path):
96
+ if not p.is_absolute():
97
+ return p
98
+ if p.is_relative_to(Path.cwd()):
99
+ relative = p.relative_to(Path.cwd())
100
+ return self.get_workdir() / relative
101
+ elif p.is_relative_to(Config().cache_location):
102
+ relative = p.relative_to(Config().cache_location)
103
+ return Path("/home/weck_cache") / relative
104
+ else:
105
+ return p
106
+ mapped = _map_path(Path(p))
107
+ if Path(p) == mapped:
108
+ return p
109
+ else:
110
+ return mapped
111
+
112
+ return tuple(map(_map_path, command))
113
+
114
+ @singledispatchmethod
115
+ def _initialize_image(self, image: str) -> str:
116
+ logger.debug("Initializing image from string %s", image)
117
+ return image
118
+
119
+ @_initialize_image.register
120
+ def _from_fm_config(self, fm_config: FmImageConfig) -> str:
121
+ logger.debug("Initializing image from FmImageConfig: %s", fm_config)
122
+ return ImageMgr().prepare_image(self, fm_config)
123
+
38
124
  @staticmethod
39
125
  def extract_image(fm: Union[str, Path], version: str, config: dict) -> str:
40
126
  image = config.get("defaults", {}).get("image", None)
41
127
 
42
128
  return parse_fm_data(fm, version).get_images().with_fallback(image)
43
129
 
130
+ @staticmethod
131
+ def _base_engine_class(config: Config):
132
+ engine = config.defaults().get("engine", "").lower()
133
+
134
+ if engine == "docker":
135
+ return Docker
136
+ if engine == "podman":
137
+ return Podman
138
+
139
+ raise ValueError(f"Unknown engine {engine}")
140
+
44
141
  @singledispatchmethod
45
142
  @staticmethod
46
143
  def from_config(config: Config) -> "Engine":
47
- engine = Podman(config.from_defaults_or_none("image"))
144
+ Base = Engine._base_engine_class(config)
145
+ engine = Base(config.from_defaults_or_none("image"))
48
146
  return Engine._prepare_engine(engine, config)
49
147
 
50
148
  @from_config.register
51
149
  @staticmethod
52
150
  def _(fm: Path, version: str, config: Config):
53
151
  image = Engine.extract_image(fm, version, config)
54
- engine = Podman(image)
152
+ Base = Engine._base_engine_class(config)
153
+ engine = Base(image)
55
154
  return Engine._prepare_engine(engine, config)
56
155
 
57
156
  @from_config.register
58
157
  @staticmethod
59
158
  def _(fm: str, version: str, config: Config):
60
159
  image = Engine.extract_image(fm, version, config)
61
- engine = Podman(image)
160
+ Base = Engine._base_engine_class(config)
161
+ engine = Base(image)
62
162
  return Engine._prepare_engine(engine, config)
63
163
 
64
164
  @from_config.register
65
165
  @staticmethod
66
166
  def _(fm: FmData, config: Config):
67
167
  image = fm.get_images().with_fallback(config.from_defaults_or_none("image"))
68
- engine = Podman(image)
168
+ Base = Engine._base_engine_class(config)
169
+ engine = Base(image)
69
170
  return Engine._prepare_engine(engine, config)
70
171
 
71
172
  @staticmethod
72
- def _prepare_engine(engine, config: dict) -> "Engine":
73
- for src, target in config.get("mount", {}).items():
173
+ def _prepare_engine(engine, config: Config) -> "Engine":
174
+ for src, target in config.mounts():
74
175
  if not Path(src).exists():
75
- logging.warning("Mount source %s does not exist. Ignoring it...", src)
176
+ logger.warning("Mount source %s does not exist. Ignoring it...", src)
76
177
  continue
77
178
  engine.mount(src, target)
78
179
 
79
180
  return engine
80
181
 
81
182
  @abstractmethod
82
- def image_from(self, containerfile: Path) -> "BuildCommand":
83
- ...
183
+ def image_from(self, containerfile: Path) -> "BuildCommand": ...
84
184
 
85
185
  class BuildCommand(ABC):
86
- @abstractmethod
87
- def base_image(self, image: str): ...
186
+ build_args: List[str] = []
88
187
 
89
188
  @abstractmethod
90
- def packages(self, packages: list[str]): ...
189
+ def __init__(self, containerfile: Path, **kwargs):
190
+ pass
91
191
 
92
- @abstractmethod
93
- def build(self): ...
192
+ def base_image(self, image: str):
193
+ self.build_args += ["--build-arg", f"BASE_IMAGE={image}"]
194
+ return self
195
+
196
+ def packages(self, packages: list[str]):
197
+ self.build_args += ["--build-arg", f"REQUIRED_PACKAGES={' '.join(packages)}"]
198
+ return self
199
+
200
+ def engine(self):
201
+ return [self._engine]
202
+
203
+ def build(self):
204
+ cmd = self.engine() + [
205
+ "build",
206
+ "-f",
207
+ self.containerfile,
208
+ *self.build_args,
209
+ ".",
210
+ ]
211
+
212
+ logging.debug("Running command: %s", cmd)
213
+
214
+ ret = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
215
+
216
+ tag = ret.stdout.decode().splitlines()[-1].strip()
217
+ logger.info("Built image %s", tag)
218
+ logger.debug("Output of build image was:\n%s", ret.stdout.decode())
219
+
220
+ return tag
94
221
 
95
222
  @staticmethod
96
223
  def _run_process(command: tuple[str, ...] | list[str]):
@@ -102,84 +229,25 @@ class Engine(ABC):
102
229
  raise NoImageError("No image set for engine.")
103
230
 
104
231
  command = self.assemble_command(command)
105
- logging.info("Running: %s", command)
232
+ logger.info("Running: %s", command)
106
233
  self._run_process(command)
107
234
 
108
- def get_workdir(self):
109
- return Path.cwd().absolute()
110
-
111
235
 
112
236
  class Podman(Engine):
113
237
  def __init__(self, image: Union[str, FmImageConfig]):
238
+ super().__init__(image)
114
239
  self._engine = "podman"
115
- self.image = self._initialize_image(image)
116
- self.extra_args = {}
117
-
118
- @singledispatchmethod
119
- def _initialize_image(self, image: str) -> str:
120
- return image
121
-
122
- @_initialize_image.register
123
- def _from_fm_config(self, fm_config: FmImageConfig) -> str:
124
- return ImageMgr().prepare_image(self, fm_config)
125
-
126
- def mount_benchexec(self, benchexec_dir: str):
127
- self.extra_args["benchexec"] = [
128
- "-v",
129
- f"{benchexec_dir.absolute()}:/benchexec",
130
- ]
131
-
132
- def mount(self, source: str, target: str):
133
- self.extra_args["mounts"] = self.extra_args.get("mounts", []) + [
134
- "-v",
135
- f"{source}:{target}",
136
- ]
137
240
 
138
241
  class PodmanBuildCommand(Engine.BuildCommand):
139
242
  def __init__(self, containerfile: Path):
140
243
  self.containerfile = containerfile
141
- self.build_args = {}
142
-
143
- def base_image(self, image: str):
144
- self.build_args["--build-arg"] = f"BASE_IMAGE={image}"
145
- return self
146
-
147
- def packages(self, packages: list[str]):
148
- self.build_args["--build-arg"] = f"REQUIRED_PACKAGES=\"{' '.join(packages)}\""
149
- return self
150
-
151
- def build(self):
152
- tag_id = uuid.uuid4().hex
153
- tag = f"fmweck/{tag_id}"
154
-
155
- ret = subprocess.run(
156
- [
157
- "podman",
158
- "build",
159
- "-t",
160
- tag,
161
- "-f",
162
- self.containerfile,
163
- *self.build_args,
164
- ".",
165
- ],
166
- check=True,
167
- )
168
-
169
- logging.debug("Build output: %s", ret.stdout)
170
- logging.info("Built image %s", tag)
171
-
172
- return tag
244
+ self._engine = "podman"
173
245
 
174
246
  def image_from(self, containerfile: Path):
175
247
  return self.PodmanBuildCommand(containerfile)
176
248
 
177
- def get_workdir(self):
178
- return Path("/home/cwd")
179
-
180
- def assemble_command(self, command: tuple[str, ...]) -> list[str]:
181
-
182
- benchexec_cap = [
249
+ def benchexec_capabilities(self):
250
+ return [
183
251
  "--annotation",
184
252
  "run.oci.keep_original_groups=1",
185
253
  "--security-opt",
@@ -190,17 +258,49 @@ class Podman(Engine):
190
258
  "/sys/fs/cgroup:/sys/fs/cgroup",
191
259
  ]
192
260
 
193
- base = [
194
- "podman",
195
- "run",
196
- ]
197
261
 
198
- if self.add_benchexec_capabilities:
199
- base += benchexec_cap
262
+ class Docker(Engine):
263
+ def __init__(self, image: Union[str, FmImageConfig]):
264
+ super().__init__(image)
265
+ logger.debug("Image: %s", self.image)
266
+ self._engine = "docker"
267
+
268
+ class DockerBuildCommand(Engine.BuildCommand):
269
+ def __init__(self, containerfile: Path, needs_sudo: bool = False):
270
+ self.containerfile = containerfile
271
+ if needs_sudo:
272
+ self._engine = "sudo docker"
273
+ else:
274
+ self._engine = "docker"
275
+
276
+ def engine(self):
277
+ return self._engine.split(" ")
200
278
 
201
- base += [
202
- "--entrypoint", '[""]',
203
- "--cap-add", "SYS_ADMIN",
279
+ def image_from(self, containerfile: Path):
280
+ return self.DockerBuildCommand(containerfile, needs_sudo=self._requires_sudo)
281
+
282
+ @cached_property
283
+ def _requires_sudo(self):
284
+ """Test if docker works without sudo."""
285
+ try:
286
+ subprocess.run(["docker", "info"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
287
+ logger.debug("Docker does not require sudo.")
288
+ return False
289
+ except subprocess.CalledProcessError:
290
+ logger.debug("Docker requires sudo.")
291
+ return True
292
+
293
+ def base_command(self):
294
+ if self._requires_sudo:
295
+ return ["sudo", "docker", "run"]
296
+ return ["docker", "run"]
297
+
298
+ def setup_command(self):
299
+ return [
300
+ "--entrypoint",
301
+ "/bin/sh",
302
+ "--cap-add",
303
+ "SYS_ADMIN",
204
304
  "-v",
205
305
  f"{Path.cwd().absolute()}:/home/cwd",
206
306
  "-v",
@@ -210,37 +310,13 @@ class Podman(Engine):
210
310
  "--rm",
211
311
  ]
212
312
 
213
- if self.interactive:
214
- base += ["-it"]
215
-
216
- for value in self.extra_args.values():
217
- if isinstance(value, list) and not isinstance(value, str):
218
- base += value
219
- else:
220
- base.append(value)
221
- _command = self._prep_command(command)
222
- return base + [self.image, *_command]
223
-
224
- def _prep_command(self, command: tuple[str, ...]) -> tuple[str, ...]:
225
- """We want to map absolute paths of the current working directory to the
226
- working directory of the container."""
227
-
228
- def _map_path(p: Union[str, Path]) -> Union[str, Path]:
229
- if isinstance(p, Path):
230
- if not p.is_absolute():
231
- return p
232
- if p.is_relative_to(Path.cwd()):
233
- relative = p.relative_to(Path.cwd())
234
- return self.get_workdir() / relative
235
- elif p.is_relative_to(Config().cache_location):
236
- relative = p.relative_to(Config().cache_location)
237
- return Path("/home/weck_cache") / relative
238
- else:
239
- return p
240
- mapped = _map_path(Path(p))
241
- if Path(p) == mapped:
242
- return p
243
- else:
244
- return mapped
245
-
246
- return tuple(map(_map_path, command))
313
+ def benchexec_capabilities(self):
314
+ return [
315
+ "--security-opt",
316
+ "seccomp=unconfined",
317
+ "--security-opt",
318
+ "apparmor=unconfined",
319
+ "--security-opt",
320
+ "label=disable",
321
+ "-v /sys/fs/cgroup:/sys/fs/cgroup",
322
+ ]
fm_weck/image_mgr.py CHANGED
@@ -5,10 +5,12 @@
5
5
  #
6
6
  # SPDX-License-Identifier: Apache-2.0
7
7
 
8
+ import logging
8
9
  from pathlib import Path
9
10
  from typing import TYPE_CHECKING
10
11
 
11
12
  from fm_tools.fmdata import FmImageConfig
13
+ from yaspin import yaspin
12
14
 
13
15
  from fm_weck import Config
14
16
 
@@ -38,9 +40,15 @@ class ImageMgr(object):
38
40
  if image.base_images and not image.required_packages:
39
41
  return image.base_images[0]
40
42
 
43
+ logging.info(
44
+ "Building image from from base image %s with packages %s", image.base_images[0], image.required_packages
45
+ )
41
46
  image_cmd = engine.image_from(CONTAINERFILE)
42
- image_cmd.packages(image.required_packages)
43
47
  image_cmd.base_image(image.base_images[0])
48
+ image_cmd.packages(image.required_packages)
49
+
50
+ with yaspin(text="Building image", color="cyan") as spinner:
51
+ tag = image_cmd.build()
52
+ spinner.ok("✅ ")
44
53
 
45
- tag = image_cmd.build()
46
- return "localhost/" + tag
54
+ return tag
@@ -5,8 +5,9 @@
5
5
  #
6
6
  # SPDX-License-Identifier: Apache-2.0
7
7
 
8
- ARG REQUIRED_PACKAGES
8
+
9
9
  ARG BASE_IMAGE
10
10
  FROM ${BASE_IMAGE}
11
11
  RUN apt-get update
12
- RUN apt-get install -y ${REQUIRED_PACKAGES}
12
+ ARG REQUIRED_PACKAGES
13
+ RUN apt-get install -y ${REQUIRED_PACKAGES} && rm -rf /var/lib/apt/lists/*
@@ -11,7 +11,8 @@ fmtools_entry_maintainers:
11
11
  - cedricrupb
12
12
 
13
13
  maintainers:
14
- - name: Marek Chalupa
14
+ - orcid: 0000-0003-1132-5516
15
+ name: Marek Chalupa
15
16
  institution: ISTA
16
17
  country: Austria
17
18
  url: null
@@ -30,6 +31,7 @@ competition_participations:
30
31
  track: "Verification"
31
32
  tool_version: "svcomp24"
32
33
  jury_member:
34
+ orcid: 0000-0003-1132-5516
33
35
  name: Marek Chalupa
34
36
  institution: ISTA
35
37
  country: Austria
@@ -46,3 +48,8 @@ techniques:
46
48
  - Portfolio
47
49
 
48
50
  frameworks_solvers: []
51
+
52
+ literature:
53
+ - doi: 10.1007/978-3-031-57256-2_20
54
+ title: "Bubaak-SpLit: Split What You Cannot Verify (Competition Contribution)"
55
+ year: 2024