fm-weck 1.1__py3-none-any.whl → 1.1.2__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"
11
+ __version__ = "1.1.2"
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:
@@ -174,20 +176,17 @@ def parse(raw_args: list[str]) -> Tuple[Callable[[], None], Namespace]:
174
176
  def help_callback():
175
177
  parser.print_help()
176
178
 
177
- try:
178
- parser.parse_args(raw_args)
179
- except SystemExit as e:
180
- _, left_over = parser.parse_known_args(raw_args)
179
+ result, left_over = parser.parse_known_args(raw_args)
181
180
 
182
- if not left_over:
183
- # Some unrecoverable error occurred
184
- raise e
181
+ if not left_over:
182
+ # Parsing went fine
183
+ return help_callback, result
185
184
 
186
- # Find the first offending argument and insert "--" before it
187
- # We do this to allow the user to pass arguments to the fm-tool without
188
- # having to specify the pseudo argument "--"
189
- idx = raw_args.index(left_over[0])
190
- raw_args.insert(idx, "--")
185
+ # Find the first offending argument and insert "--" before it
186
+ # We do this to allow the user to pass arguments to the fm-tool without
187
+ # having to specify the pseudo argument "--"
188
+ idx = raw_args.index(left_over[0])
189
+ raw_args.insert(idx, "--")
191
190
 
192
191
  return help_callback, parser.parse_args(raw_args)
193
192
 
@@ -235,22 +234,23 @@ def set_log_level(loglevel: Optional[str], config: dict[str, Any]):
235
234
  level = "WARNING"
236
235
  level = loglevel.upper() if loglevel else config.get("logging", {}).get("level", level)
237
236
  logging.basicConfig(level=level)
237
+ logging.getLogger("httpcore").setLevel("WARNING")
238
238
 
239
239
 
240
240
  def main_run(args: argparse.Namespace):
241
241
  if not args.TOOL:
242
- logging.error("No fm-tool given. Aborting...")
242
+ logger.error("No fm-tool given. Aborting...")
243
243
  return 1
244
244
  try:
245
245
  fm_data = resolve_tool(args.TOOL)
246
246
  except KeyError:
247
- logging.error("Unknown tool %s", args.TOOL)
247
+ logger.error("Unknown tool %s", args.TOOL)
248
248
  return 1
249
249
 
250
250
  try:
251
251
  property_path = resolve_property(args.property) if args.property else None
252
252
  except KeyError:
253
- logging.error("Unknown property %s", args.property)
253
+ logger.error("Unknown property %s", args.property)
254
254
  return 1
255
255
 
256
256
  run_guided(
@@ -267,12 +267,12 @@ def main_run(args: argparse.Namespace):
267
267
 
268
268
  def main_manual(args: argparse.Namespace):
269
269
  if not args.TOOL:
270
- logging.error("No fm-tool given. Aborting...")
270
+ logger.error("No fm-tool given. Aborting...")
271
271
  return 1
272
272
  try:
273
273
  fm_data = resolve_tool(args.TOOL)
274
274
  except KeyError:
275
- logging.error("Unknown tool %s", args.TOOL)
275
+ logger.error("Unknown tool %s", args.TOOL)
276
276
  return 1
277
277
 
278
278
  run_manual(
@@ -289,7 +289,7 @@ def main_install(args: argparse.Namespace):
289
289
  try:
290
290
  fm_data = resolve_tool(tool)
291
291
  except KeyError:
292
- logging.error("Unknown tool %s. Skipping installation...", tool)
292
+ logger.error("Unknown tool %s. Skipping installation...", tool)
293
293
  continue
294
294
 
295
295
  setup_fm_tool(
@@ -306,7 +306,7 @@ def main_shell(args: argparse.Namespace):
306
306
  try:
307
307
  fm_data = resolve_tool(args.TOOL)
308
308
  except KeyError:
309
- logging.error("Unknown tool %s", args.fm_data)
309
+ logger.error("Unknown tool %s", args.fm_data)
310
310
  return 1
311
311
  engine = Engine.from_config(fm_data, args.TOOL.version, Config())
312
312
  engine.interactive = True
@@ -339,7 +339,7 @@ printf '[defaults]\\nimage = "<your_image>"' > .weck
339
339
 
340
340
  Replace <your_image> with the image you want to use.
341
341
  """
342
- logging.error(text)
342
+ logger.error(text)
343
343
  return
344
344
 
345
345
  text = """
@@ -353,7 +353,7 @@ image = "your_image"
353
353
  to your .weck file.
354
354
  """
355
355
 
356
- logging.error(text, tool, config)
356
+ logger.error(text, tool, config)
357
357
 
358
358
 
359
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/*