olcf-velocity 0.2.dev0__tar.gz → 0.3.dev0__tar.gz

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 (25) hide show
  1. olcf_velocity-0.3.dev0/PKG-INFO +40 -0
  2. olcf_velocity-0.3.dev0/README.md +25 -0
  3. {olcf_velocity-0.2.dev0 → olcf_velocity-0.3.dev0}/pyproject.toml +2 -2
  4. olcf_velocity-0.3.dev0/src/olcf_velocity.egg-info/PKG-INFO +40 -0
  5. olcf_velocity-0.3.dev0/src/velocity/__init__.py +132 -0
  6. {olcf_velocity-0.2.dev0 → olcf_velocity-0.3.dev0}/src/velocity/_backends.py +109 -44
  7. {olcf_velocity-0.2.dev0 → olcf_velocity-0.3.dev0}/src/velocity/_build.py +61 -42
  8. {olcf_velocity-0.2.dev0 → olcf_velocity-0.3.dev0}/src/velocity/_config.py +6 -2
  9. {olcf_velocity-0.2.dev0 → olcf_velocity-0.3.dev0}/src/velocity/_tools.py +18 -34
  10. olcf_velocity-0.2.dev0/PKG-INFO +0 -31
  11. olcf_velocity-0.2.dev0/README.md +0 -16
  12. olcf_velocity-0.2.dev0/src/olcf_velocity.egg-info/PKG-INFO +0 -31
  13. olcf_velocity-0.2.dev0/src/velocity/__init__.py +0 -5
  14. {olcf_velocity-0.2.dev0 → olcf_velocity-0.3.dev0}/LICENSE.txt +0 -0
  15. {olcf_velocity-0.2.dev0 → olcf_velocity-0.3.dev0}/setup.cfg +0 -0
  16. {olcf_velocity-0.2.dev0 → olcf_velocity-0.3.dev0}/src/olcf_velocity.egg-info/SOURCES.txt +0 -0
  17. {olcf_velocity-0.2.dev0 → olcf_velocity-0.3.dev0}/src/olcf_velocity.egg-info/dependency_links.txt +0 -0
  18. {olcf_velocity-0.2.dev0 → olcf_velocity-0.3.dev0}/src/olcf_velocity.egg-info/requires.txt +0 -0
  19. {olcf_velocity-0.2.dev0 → olcf_velocity-0.3.dev0}/src/olcf_velocity.egg-info/top_level.txt +0 -0
  20. {olcf_velocity-0.2.dev0 → olcf_velocity-0.3.dev0}/src/velocity/__main__.py +0 -0
  21. {olcf_velocity-0.2.dev0 → olcf_velocity-0.3.dev0}/src/velocity/_exceptions.py +0 -0
  22. {olcf_velocity-0.2.dev0 → olcf_velocity-0.3.dev0}/src/velocity/_graph.py +0 -0
  23. {olcf_velocity-0.2.dev0 → olcf_velocity-0.3.dev0}/src/velocity/_print.py +0 -0
  24. {olcf_velocity-0.2.dev0 → olcf_velocity-0.3.dev0}/tests/test__config.py +0 -0
  25. {olcf_velocity-0.2.dev0 → olcf_velocity-0.3.dev0}/tests/test__graph.py +0 -0
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.2
2
+ Name: olcf-velocity
3
+ Version: 0.3.dev0
4
+ Summary: A container build manager
5
+ Project-URL: Homepage, https://github.com/olcf/velocity
6
+ Project-URL: Issues, https://github.com/olcf/velocity/issues
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE.txt
10
+ Requires-Dist: pyyaml
11
+ Requires-Dist: networkx
12
+ Requires-Dist: colorama
13
+ Requires-Dist: loguru
14
+ Requires-Dist: typing_extensions
15
+
16
+ ![icon.drawio.png](misc/artwork/icon_name.drawio.png)
17
+
18
+ [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/olcf/velocity/tests.yaml?label=tests)](https://github.com/olcf/velocity/actions/workflows/tests.yaml)
19
+ [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/olcf/velocity/docs.yaml?label=docs)](https://github.com/olcf/velocity/actions/workflows/docs.yaml)
20
+ [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/olcf/velocity/linter.yaml?label=linter)](https://github.com/olcf/velocity/actions/workflows/linter.yaml)
21
+ [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/olcf/velocity/build.yaml?label=build&color=https%3A%2F%2Fgithub.com%2Folcf%2Fvelocity%2Factions%2Fworkflows%2Flinter.yaml)](https://github.com/olcf/velocity/actions/workflows/build.yaml)
22
+ [![GitHub Release](https://img.shields.io/github/v/release/olcf/velocity?label=github&color=%23782D84)](https://github.com/olcf/velocity/releases)
23
+ [![PyPI - Version](https://img.shields.io/pypi/v/olcf-velocity?color=%23FFD242)](https://pypi.org/project/olcf-velocity)
24
+ [![Spack](https://img.shields.io/spack/v/py-olcf-velocity?color=%230F3A80)](https://packages.spack.io/package.html?name=py-olcf-velocity)
25
+
26
+ -----------------------------------------------------------
27
+
28
+ ## Description
29
+ Velocity is a tool to help with the maintenance of container build scripts on
30
+ multiple systems, backends (e.g podman or apptainer) and distros.
31
+
32
+ ## Documentation
33
+ See <https://olcf.github.io/velocity/>.
34
+
35
+ ## Installation
36
+ ``` commandline
37
+ pip install olcf-velocity
38
+ alias velocity="python3 -m velocity"
39
+ velocity
40
+ ```
@@ -0,0 +1,25 @@
1
+ ![icon.drawio.png](misc/artwork/icon_name.drawio.png)
2
+
3
+ [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/olcf/velocity/tests.yaml?label=tests)](https://github.com/olcf/velocity/actions/workflows/tests.yaml)
4
+ [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/olcf/velocity/docs.yaml?label=docs)](https://github.com/olcf/velocity/actions/workflows/docs.yaml)
5
+ [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/olcf/velocity/linter.yaml?label=linter)](https://github.com/olcf/velocity/actions/workflows/linter.yaml)
6
+ [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/olcf/velocity/build.yaml?label=build&color=https%3A%2F%2Fgithub.com%2Folcf%2Fvelocity%2Factions%2Fworkflows%2Flinter.yaml)](https://github.com/olcf/velocity/actions/workflows/build.yaml)
7
+ [![GitHub Release](https://img.shields.io/github/v/release/olcf/velocity?label=github&color=%23782D84)](https://github.com/olcf/velocity/releases)
8
+ [![PyPI - Version](https://img.shields.io/pypi/v/olcf-velocity?color=%23FFD242)](https://pypi.org/project/olcf-velocity)
9
+ [![Spack](https://img.shields.io/spack/v/py-olcf-velocity?color=%230F3A80)](https://packages.spack.io/package.html?name=py-olcf-velocity)
10
+
11
+ -----------------------------------------------------------
12
+
13
+ ## Description
14
+ Velocity is a tool to help with the maintenance of container build scripts on
15
+ multiple systems, backends (e.g podman or apptainer) and distros.
16
+
17
+ ## Documentation
18
+ See <https://olcf.github.io/velocity/>.
19
+
20
+ ## Installation
21
+ ``` commandline
22
+ pip install olcf-velocity
23
+ alias velocity="python3 -m velocity"
24
+ velocity
25
+ ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "olcf-velocity"
7
- version = "0.2.dev"
7
+ version = "0.3.dev"
8
8
  description = "A container build manager"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -51,7 +51,7 @@ exclude = [
51
51
  ]
52
52
  line-length = 120
53
53
  indent-width = 4
54
- target-version = "py310"
54
+ target-version = "py312"
55
55
 
56
56
  [tool.ruff.lint]
57
57
  select = ["E4", "E7", "E9", "F"]
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.2
2
+ Name: olcf-velocity
3
+ Version: 0.3.dev0
4
+ Summary: A container build manager
5
+ Project-URL: Homepage, https://github.com/olcf/velocity
6
+ Project-URL: Issues, https://github.com/olcf/velocity/issues
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE.txt
10
+ Requires-Dist: pyyaml
11
+ Requires-Dist: networkx
12
+ Requires-Dist: colorama
13
+ Requires-Dist: loguru
14
+ Requires-Dist: typing_extensions
15
+
16
+ ![icon.drawio.png](misc/artwork/icon_name.drawio.png)
17
+
18
+ [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/olcf/velocity/tests.yaml?label=tests)](https://github.com/olcf/velocity/actions/workflows/tests.yaml)
19
+ [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/olcf/velocity/docs.yaml?label=docs)](https://github.com/olcf/velocity/actions/workflows/docs.yaml)
20
+ [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/olcf/velocity/linter.yaml?label=linter)](https://github.com/olcf/velocity/actions/workflows/linter.yaml)
21
+ [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/olcf/velocity/build.yaml?label=build&color=https%3A%2F%2Fgithub.com%2Folcf%2Fvelocity%2Factions%2Fworkflows%2Flinter.yaml)](https://github.com/olcf/velocity/actions/workflows/build.yaml)
22
+ [![GitHub Release](https://img.shields.io/github/v/release/olcf/velocity?label=github&color=%23782D84)](https://github.com/olcf/velocity/releases)
23
+ [![PyPI - Version](https://img.shields.io/pypi/v/olcf-velocity?color=%23FFD242)](https://pypi.org/project/olcf-velocity)
24
+ [![Spack](https://img.shields.io/spack/v/py-olcf-velocity?color=%230F3A80)](https://packages.spack.io/package.html?name=py-olcf-velocity)
25
+
26
+ -----------------------------------------------------------
27
+
28
+ ## Description
29
+ Velocity is a tool to help with the maintenance of container build scripts on
30
+ multiple systems, backends (e.g podman or apptainer) and distros.
31
+
32
+ ## Documentation
33
+ See <https://olcf.github.io/velocity/>.
34
+
35
+ ## Installation
36
+ ``` commandline
37
+ pip install olcf-velocity
38
+ alias velocity="python3 -m velocity"
39
+ velocity
40
+ ```
@@ -0,0 +1,132 @@
1
+ from colorama import Fore, Style
2
+ from loguru import logger; logger.disable("velocity") # noqa: E702 # disable logging at the module level
3
+
4
+ from velocity._config import config # noqa: E402
5
+ from velocity._graph import ImageRepo # noqa: E402
6
+ from velocity._build import ImageBuilder # noqa: E402
7
+ from velocity._print import TextBlock, header_print, indent_print # noqa: E402
8
+
9
+
10
+ # config functions
11
+ def get_system() -> str:
12
+ """Get the system.
13
+
14
+ Returns
15
+ -------
16
+ str
17
+ The current system.
18
+ """
19
+
20
+ return config.get("velocity:system")
21
+
22
+
23
+ def set_system(system: str) -> None:
24
+ """Set the system.
25
+
26
+ Parameters
27
+ ----------
28
+ system : str
29
+ New value for system.
30
+ """
31
+
32
+ config.set("velocity:system", str(system))
33
+
34
+
35
+ def get_backend() -> str:
36
+ """Get the backend.
37
+
38
+ Returns
39
+ -------
40
+ str
41
+ The current backend.
42
+ """
43
+
44
+ return config.get("velocity:backend")
45
+
46
+
47
+ def set_backend(backend: str) -> None:
48
+ """Set the backend.
49
+
50
+ Parameters
51
+ ----------
52
+ backend : str
53
+ New value for backend.
54
+ """
55
+
56
+ config.set("velocity:backend", str(backend))
57
+
58
+
59
+ def get_distro() -> str:
60
+ """Get the distro.
61
+
62
+ Returns
63
+ -------
64
+ str
65
+ The current distro.
66
+ """
67
+
68
+ return config.get("velocity:distro")
69
+
70
+
71
+ def set_distro(distro: str) -> None:
72
+ """Set the distro.
73
+
74
+ Parameters
75
+ ----------
76
+ distro : str
77
+ New value for distro.
78
+ """
79
+
80
+ config.set("velocity:distro", str(distro))
81
+
82
+
83
+ def build(
84
+ targets: str,
85
+ name: str = None,
86
+ dry_run: bool = False,
87
+ leave_tags: bool = False,
88
+ verbose: bool = False,
89
+ clean: bool = False,
90
+ ) -> None:
91
+ """Build an image.
92
+
93
+ Parameters
94
+ ----------
95
+ targets: str
96
+ A string of build targets e.g. '`gcc@12.4 rocm@5`'.
97
+ name: str
98
+ Name of complete image.
99
+ dry_run: bool
100
+ Dry run build system.
101
+ leave_tags: bool
102
+ Do not clean up intermediate build tags (only applies to dockerish backends).
103
+ verbose: bool
104
+ Print helpful debug/runtime information.
105
+ clean: bool
106
+ Remove cached files in the build directory.
107
+ """
108
+
109
+ imageRepo = ImageRepo()
110
+ for p in config.get("velocity:image_path").strip(":").split(":"):
111
+ imageRepo.import_from_dir(p)
112
+
113
+ # get recipe
114
+ recipe = imageRepo.create_build_recipe(targets.split())[0]
115
+
116
+ # print build specs
117
+ header_print([TextBlock("Build Order:")])
118
+ for r in recipe:
119
+ indent_print([TextBlock(f"{r.name}@{r.version}-{r.id}", fore=Fore.MAGENTA, style=Style.BRIGHT)])
120
+ print() # newline
121
+
122
+ # prep builder
123
+ builder = ImageBuilder(
124
+ recipe, build_name=name, dry_run=dry_run, remove_tags=not leave_tags, verbose=verbose, clean_build_dir=clean
125
+ )
126
+
127
+ # build
128
+ builder.build()
129
+
130
+
131
+ # visible attributes
132
+ __all__ = ["get_system", "set_system", "get_backend", "set_backend", "get_distro", "set_distro", "build"]
@@ -1,20 +1,23 @@
1
1
  """Velocity backends."""
2
2
 
3
- from re import Match as re_Match, sub as re_sub, match as re_match
4
- from loguru import logger
5
- from shutil import which as shutil_which
6
- from pathlib import Path
7
3
  from abc import abstractmethod
8
- from ._exceptions import (
9
- RepeatedSection,
4
+ from pathlib import Path
5
+ from re import Match as re_Match, match as re_match, sub as re_sub
6
+ from shutil import which as shutil_which
7
+ from subprocess import run as subprocess_run
8
+
9
+ from loguru import logger
10
+
11
+ from velocity._exceptions import (
12
+ BackendNotAvailable,
13
+ BackendNotSupported,
10
14
  LineOutsideOfSection,
15
+ RepeatedSection,
11
16
  TemplateSyntaxError,
12
- BackendNotSupported,
13
- BackendNotAvailable,
14
17
  )
15
- from ._config import config
16
- from ._graph import Image
17
- from ._tools import OurABCMeta, trace_function
18
+ from velocity._config import config
19
+ from velocity._graph import Image
20
+ from velocity._tools import OurABCMeta, trace_function
18
21
 
19
22
 
20
23
  @trace_function
@@ -44,6 +47,11 @@ class Backend(metaclass=OurABCMeta):
44
47
  "@post",
45
48
  ]
46
49
 
50
+ existing_builds_cache: dict = dict()
51
+
52
+ name: str = "backend"
53
+ executable: str = "true"
54
+
47
55
  @classmethod
48
56
  @trace_function
49
57
  def _get_sections(cls, template: list[str]) -> dict[str, list[str]]:
@@ -89,6 +97,7 @@ class Backend(metaclass=OurABCMeta):
89
97
  @trace_function
90
98
  def _filter_content(cls, image: Image, text: str) -> str:
91
99
  """Filter conditionals and white space from a template line."""
100
+
92
101
  # handle conditionals
93
102
  res: re_Match[str] = re_match(r".*(\?\?([\S ]*)\|>(.*)\?\?).*", text)
94
103
  if res is not None:
@@ -108,6 +117,7 @@ class Backend(metaclass=OurABCMeta):
108
117
  @trace_function
109
118
  def _load_template(cls, image: Image, variables: dict[str, str]) -> list[str]:
110
119
  """Load a template and parse it."""
120
+
111
121
  template: list[str] = list()
112
122
  with open(
113
123
  Path(image.path).joinpath("templates", "{}.vtmp".format(image.template)),
@@ -120,12 +130,9 @@ class Backend(metaclass=OurABCMeta):
120
130
  template.append(fcon)
121
131
  return template
122
132
 
123
- def __init__(self, name: str, executable: str) -> None:
124
- self.name: str = name
125
- self.executable: str = executable
126
-
127
133
  def generate_script(self, image: Image, variables: dict[str, str]) -> list[str]:
128
- """Generate a build script e.g. .dockerfile/.def"""
134
+ """Generate a build script."""
135
+
129
136
  logger.debug("Variables: {}".format(variables))
130
137
  template: list[str] = self._load_template(image, variables)
131
138
  sections: dict[str, list[str]] = self._get_sections(template)
@@ -179,6 +186,7 @@ class Backend(metaclass=OurABCMeta):
179
186
 
180
187
  def is_available(self) -> bool:
181
188
  """Check if the current system has the requested backend."""
189
+
182
190
  if shutil_which(self.executable) is None:
183
191
  return False
184
192
  return True
@@ -214,13 +222,14 @@ class Backend(metaclass=OurABCMeta):
214
222
  @classmethod
215
223
  def _literal_section(cls, contents: list[str]) -> list[str]:
216
224
  """Handle literal sections."""
225
+
217
226
  ret: list = [""]
218
227
  for ln in contents:
219
228
  ret.append(ln.lstrip("|"))
220
229
  return ret
221
230
 
222
231
  @abstractmethod
223
- def generate_build_cmd(self, src: str, dest: str, args: list[str] = None) -> str:
232
+ def generate_build_cmd(self, src: str, dest: str, args: list[str] = None) -> list[str]:
224
233
  """Generate CLI command to build."""
225
234
 
226
235
  @abstractmethod
@@ -243,8 +252,8 @@ class Backend(metaclass=OurABCMeta):
243
252
  class Apptainer(Backend):
244
253
  """Apptainer backend."""
245
254
 
246
- def __init__(self):
247
- super().__init__(name="apptainer", executable="apptainer")
255
+ name = "apptainer"
256
+ executable = "apptainer"
248
257
 
249
258
  def _from(self, contents: list[str]) -> list[str]:
250
259
  ret: list[str] = list()
@@ -270,7 +279,7 @@ class Apptainer(Backend):
270
279
  ret.append("From: {}".format(res["main"]))
271
280
  case _:
272
281
  raise TemplateSyntaxError("Unknown bootstrap type '{}' in @from!".format(res["bootstrap"]))
273
- else: # if the bootstrap type was not specified
282
+ else: # if the bootstrap type was not specified
274
283
  if re_match(r"^.*\.sif$", res["main"]):
275
284
  logger.debug("Template @from source identified as 'localimage'")
276
285
  ret.append("Bootstrap: localimage")
@@ -335,7 +344,7 @@ class Apptainer(Backend):
335
344
  def _entry(self, contents: list[str]) -> list[str]:
336
345
  return ["", "%runscript", "{}".format(contents[0])]
337
346
 
338
- def generate_build_cmd(self, src: str, dest: str, args: list = None) -> str:
347
+ def generate_build_cmd(self, src: str, dest: str, args: list = None) -> list[str]:
339
348
  cmd: list[str] = ["{} build".format(self.executable)]
340
349
  # arguments
341
350
  if args is not None and len(args) > 0:
@@ -344,7 +353,7 @@ class Apptainer(Backend):
344
353
  cmd.append("{}".format(dest))
345
354
  # script
346
355
  cmd.append("{}".format(src))
347
- return " ".join(_ for _ in cmd) + ";"
356
+ return [" ".join(_ for _ in cmd) + ";"]
348
357
 
349
358
  def format_image_name(self, path: Path, tag: str) -> str:
350
359
  return "{}{}".format(Path.joinpath(path, tag), ".sif" if ".sif" not in tag else "")
@@ -353,9 +362,12 @@ class Apptainer(Backend):
353
362
  return "echo"
354
363
 
355
364
  def build_exists(self, name: str) -> bool:
356
- if Path(name).is_file():
357
- return True
358
- return False
365
+ if name not in self.existing_builds_cache:
366
+ if Path(name).is_file():
367
+ self.existing_builds_cache[name] = True
368
+ else:
369
+ self.existing_builds_cache[name] = False
370
+ return self.existing_builds_cache[name]
359
371
 
360
372
  def generate_final_image_cmd(self, src: str, dest: str) -> str:
361
373
  return "cp {} {}".format(src, dest)
@@ -364,8 +376,8 @@ class Apptainer(Backend):
364
376
  class Docker(Backend):
365
377
  """Docker backend."""
366
378
 
367
- def __init__(self):
368
- super().__init__(name="docker", executable="docker")
379
+ name = "docker"
380
+ executable = "docker"
369
381
 
370
382
  def _from(self, contents: list[str]) -> list[str]:
371
383
  return [f"FROM {contents[0]}"]
@@ -416,7 +428,7 @@ class Docker(Backend):
416
428
  ln += " "
417
429
  ln += alt_cmd
418
430
  # add '&& \\' to all but the last line
419
- if cmd != contents[-1] and cmd[-1] != '\\': # ignore line that end in an escape
431
+ if cmd != contents[-1] and cmd[-1] != "\\": # ignore line that end in an escape
420
432
  ln += " && \\"
421
433
  ret.append(ln)
422
434
  return ret
@@ -433,7 +445,7 @@ class Docker(Backend):
433
445
  else:
434
446
  # indent following lines
435
447
  ln += " "
436
- ln += f"{parts[0]}=\"{env.lstrip(parts[0]).strip(' ')}\""
448
+ ln += f'{parts[0]}="{env.lstrip(parts[0]).strip(" ")}"'
437
449
  # add '\\' to all but the last line
438
450
  if env != contents[-1]:
439
451
  ln += " \\"
@@ -454,7 +466,7 @@ class Docker(Backend):
454
466
  else:
455
467
  # indent following lines
456
468
  ln += " "
457
- ln += f"{parts[0]}=\"{label.lstrip(parts[0]).strip(' ')}\""
469
+ ln += f'{parts[0]}="{label.lstrip(parts[0]).strip(" ")}"'
458
470
  # add '\\' to all but the last line
459
471
  if label != contents[-1]:
460
472
  ln += " \\"
@@ -464,7 +476,7 @@ class Docker(Backend):
464
476
  def _entry(self, contents: list[str]) -> list[str]:
465
477
  return ["", "ENTRYPOINT {}".format(contents[0].split())]
466
478
 
467
- def generate_build_cmd(self, src: str, dest: str, args: list = None) -> str:
479
+ def generate_build_cmd(self, src: str, dest: str, args: list = None) -> list[str]:
468
480
  cmd: list[str] = ["{} build".format(self.executable)]
469
481
  # arguments
470
482
  if args is not None and len(args) > 0:
@@ -475,7 +487,7 @@ class Docker(Backend):
475
487
  cmd.append("-t {}".format(dest))
476
488
  # build dir
477
489
  cmd.append(".")
478
- return " ".join(_ for _ in cmd) + ";"
490
+ return [" ".join(_ for _ in cmd) + ";"]
479
491
 
480
492
  def format_image_name(self, path: Path, tag: str) -> str:
481
493
  return "{}{}{}".format("localhost/" if "/" not in tag else "", tag, ":latest" if ":" not in tag else "")
@@ -484,39 +496,92 @@ class Docker(Backend):
484
496
  return "{} rmi {}".format(self.executable, name)
485
497
 
486
498
  def build_exists(self, name: str) -> bool:
487
- return False
499
+ if name not in self.existing_builds_cache:
500
+ res = subprocess_run(
501
+ f"{self.executable} image ls -n" + " | awk '{print $1\":\"$2}'" + f" | grep {name}",
502
+ shell=True,
503
+ capture_output=True,
504
+ )
505
+ if res.returncode == 0:
506
+ self.existing_builds_cache[name] = True
507
+ else:
508
+ self.existing_builds_cache[name] = False
509
+ return self.existing_builds_cache[name]
488
510
 
489
511
  def generate_final_image_cmd(self, src: str, dest: str) -> str:
490
512
  return "{} tag {} {}".format(self.executable, src, dest)
491
513
 
492
514
 
515
+ class OpenShift(Docker):
516
+ """Openshift-CI backend. Inherit from Docker because openshift uses docker as the container runtime."""
517
+
518
+ name = "openshift"
519
+ executable = "oc"
520
+
521
+ def generate_build_cmd(self, src: str, dest: str, args: list = None) -> list[str]:
522
+ arguments = " " + " ".join(_ for _ in args) if args is not None else ""
523
+ cmd: list[str] = [
524
+ "cp {} {};".format(src, re_sub(r"(script$)", "Dockerfile", src)), # copy script to Dockerfile
525
+ "if ! {} get buildconfigs {}; then".format(self.executable, dest), # create new build config if none exist
526
+ " {} new-build {} --name={} --to={}:latest;".format(
527
+ self.executable, re_sub(r"(/script$)", "", src), dest, dest
528
+ ),
529
+ "fi;",
530
+ "{} start-build {} --from-dir={} --follow{};".format( # run build
531
+ self.executable, dest, re_sub(r"(/script$)", "", src), arguments
532
+ ),
533
+ "while ! {} get imagetags {}:latest; do".format(self.executable, dest), # wait for image to be pushed
534
+ " sleep 10;",
535
+ "done;",
536
+ ]
537
+ return cmd
538
+
539
+ def format_image_name(self, path: Path, tag: str) -> str:
540
+ return "v-" + tag
541
+
542
+ def clean_up_old_image_tag(self, name: str) -> str:
543
+ return "{} delete buildconfigs {}; {} delete imagestream {};".format(
544
+ self.executable, name, self.executable, name
545
+ )
546
+
547
+ def build_exists(self, name: str) -> bool:
548
+ if name not in self.existing_builds_cache:
549
+ res = subprocess_run(
550
+ "{} get imagetags {}:latest;".format(self.executable, name), shell=True, capture_output=True
551
+ )
552
+ if res.returncode == 0:
553
+ self.existing_builds_cache[name] = True
554
+ else:
555
+ self.existing_builds_cache[name] = False
556
+ return self.existing_builds_cache[name]
557
+
558
+ def generate_final_image_cmd(self, src: str, dest: str) -> str:
559
+ return "{} tag {}:latest {}:latest;".format(self.executable, src, dest)
560
+
561
+
493
562
  class Podman(Docker):
494
563
  """Podman backend. Inherit from Docker because for our purposes they are the same."""
495
564
 
496
- def __init__(self):
497
- super().__init__()
498
- # override name and executable
499
- self.name = "podman"
500
- self.executable = "podman"
565
+ name = "podman"
566
+ executable = "podman"
501
567
 
502
568
 
503
569
  class Singularity(Apptainer):
504
570
  """Singularity backend. Inherit from Apptainer because that is what Singularity really is."""
505
571
 
506
- def __init__(self):
507
- super().__init__()
508
- # override name and executable
509
- self.name = "singularity"
510
- self.executable = "singularity"
572
+ name = "singularity"
573
+ executable = "singularity"
511
574
 
512
575
 
513
576
  @trace_function
514
577
  def get_backend() -> Backend:
515
- backend = config.get("velocity:backend")
578
+ backend = config.get("velocity:backend").lower()
516
579
  if backend == "apptainer":
517
580
  b = Apptainer()
518
581
  elif backend == "docker":
519
582
  b = Docker()
583
+ elif backend == "openshift":
584
+ b = OpenShift()
520
585
  elif backend == "podman":
521
586
  b = Podman()
522
587
  elif backend == "singularity":
@@ -1,20 +1,23 @@
1
1
  """Build velocity images."""
2
2
 
3
- import datetime
4
- import shutil
5
- import os
3
+ from datetime import datetime, timedelta
4
+ from os import chdir, cpu_count
5
+ from pathlib import Path
6
+ from platform import processor as arch
7
+ from shutil import copy as shutil_copy, copytree, rmtree
8
+ from threading import Thread
6
9
  from timeit import default_timer as timer
7
- from subprocess import Popen, PIPE
8
10
  from queue import SimpleQueue
9
- from threading import Thread
10
- from pathlib import Path
11
+ from subprocess import PIPE, Popen
12
+
11
13
  from colorama import Fore, Style
12
- from platform import processor as arch
13
- from ._config import config
14
- from ._graph import Image
15
- from ._print import header_print, indent_print, TextBlock
16
- from ._backends import get_backend, Backend
17
- from ._tools import OurMeta, trace_function
14
+ from loguru import logger
15
+
16
+ from velocity._config import config
17
+ from velocity._graph import Image
18
+ from velocity._print import TextBlock, header_print, indent_print
19
+ from velocity._backends import Backend, get_backend
20
+ from velocity._tools import OurMeta, trace_function
18
21
 
19
22
 
20
23
  @trace_function
@@ -33,6 +36,7 @@ def read_pipe(pipe: PIPE, topic: SimpleQueue, prefix: str, log: SimpleQueue) ->
33
36
  def run(cmd: str, log_file: Path = None, verbose: bool = False) -> None:
34
37
  """Run a system command logging all output to a file and print if verbose."""
35
38
  # open log file (set to False if none is provided)
39
+ logger.debug("Running command: {}".format(cmd))
36
40
  file = open(log_file, "w") if log_file is not None else False
37
41
 
38
42
  log = SimpleQueue()
@@ -101,13 +105,19 @@ class ImageBuilder(metaclass=OurMeta):
101
105
  self.build_dir = Path(config.get("velocity:build_dir"))
102
106
  self.build_dir.mkdir(mode=0o777, parents=True, exist_ok=True)
103
107
 
104
- self.variables: dict[str, str] = dict()
105
- for i in self.build_units:
106
- self.variables["__{}__version__".format(i.name)] = i.version.__str__()
107
- self.variables["__{}__version_major__".format(i.name)] = i.version.major.__str__()
108
- self.variables["__{}__version_minor__".format(i.name)] = i.version.minor.__str__()
109
- self.variables["__{}__version_patch__".format(i.name)] = i.version.patch.__str__()
110
- self.variables["__{}__version_suffix__".format(i.name)] = i.version.suffix.__str__()
108
+ self.variables: dict[str, str] = {
109
+ "__backend__": self.backend_engine.name,
110
+ "__backend_executable__": self.backend_engine.executable,
111
+ "__threads__": str(int(cpu_count() * 0.75) if int(cpu_count() * 0.75) < 16 else 16),
112
+ "__arch__": arch(),
113
+ "__timestamp__": str(datetime.now()),
114
+ }
115
+ for u in self.build_units:
116
+ self.variables["__{}__version__".format(u.name)] = str(u.version)
117
+ self.variables["__{}__version_major__".format(u.name)] = str(u.version.major)
118
+ self.variables["__{}__version_minor__".format(u.name)] = str(u.version.minor)
119
+ self.variables["__{}__version_patch__".format(u.name)] = str(u.version.patch)
120
+ self.variables["__{}__version_suffix__".format(u.name)] = str(u.version.suffix)
111
121
 
112
122
  def build(self) -> None:
113
123
  """Launch image builds."""
@@ -118,7 +128,7 @@ class ImageBuilder(metaclass=OurMeta):
118
128
  if self.clean_build_dir:
119
129
  for entry in self.build_dir.iterdir():
120
130
  if entry.is_dir():
121
- shutil.rmtree(entry)
131
+ rmtree(entry)
122
132
  else:
123
133
  entry.unlink()
124
134
 
@@ -135,7 +145,7 @@ class ImageBuilder(metaclass=OurMeta):
135
145
  tag = str(
136
146
  self.build_name
137
147
  if self.build_name is not None
138
- else "{}__{}-{}".format(
148
+ else "{}_{}-{}".format(
139
149
  "_".join(f"{bu.name}-{bu.version}" for bu in reversed(self.build_units)),
140
150
  config.get("velocity:system"),
141
151
  config.get("velocity:distro"),
@@ -152,7 +162,7 @@ class ImageBuilder(metaclass=OurMeta):
152
162
  run(self.backend_engine.clean_up_old_image_tag(bn))
153
163
 
154
164
  # go back to the starting dir
155
- os.chdir(pwd)
165
+ chdir(pwd)
156
166
 
157
167
  def _build_image(self, unit: Image, src_image: str, name: str):
158
168
  """Build an individual image."""
@@ -171,7 +181,7 @@ class ImageBuilder(metaclass=OurMeta):
171
181
  # create build dir and go to it
172
182
  build_sub_dir = Path.joinpath(self.build_dir, "{}-{}-{}".format(unit.name, unit.version, unit.id))
173
183
  build_sub_dir.mkdir(mode=0o744, exist_ok=True)
174
- os.chdir(build_sub_dir)
184
+ chdir(build_sub_dir)
175
185
 
176
186
  # copy additional files
177
187
  if len(unit.files) > 0:
@@ -192,9 +202,9 @@ class ImageBuilder(metaclass=OurMeta):
192
202
  ]
193
203
  )
194
204
  if Path.joinpath(unit.path, "files", entry).is_dir():
195
- shutil.copytree(Path.joinpath(unit.path, "files", entry), Path.joinpath(build_sub_dir, entry))
205
+ copytree(Path.joinpath(unit.path, "files", entry), Path.joinpath(build_sub_dir, entry))
196
206
  else:
197
- shutil.copy(Path.joinpath(unit.path, "files", entry), Path.joinpath(build_sub_dir, entry))
207
+ shutil_copy(Path.joinpath(unit.path, "files", entry), Path.joinpath(build_sub_dir, entry))
198
208
 
199
209
  # parse template and create script...
200
210
  header_print([TextBlock(unit.id, fore=Fore.RED, style=Style.BRIGHT), TextBlock(": GENERATING SCRIPT ...")])
@@ -207,21 +217,19 @@ class ImageBuilder(metaclass=OurMeta):
207
217
  )
208
218
 
209
219
  # get and update script variables
210
- script_variables = unit.variables.copy()
211
- script_variables.update({"__name__": unit.name})
212
- script_variables.update({"__version__": str(unit.version)})
213
- script_variables.update({"__version_major__": str(unit.version.major)})
214
- script_variables.update({"__version_minor__": str(unit.version.minor)})
215
- script_variables.update({"__version_patch__": str(unit.version.patch)})
216
- script_variables.update({"__version_suffix__": str(unit.version.suffix)})
217
- script_variables.update({"__timestamp__": str(datetime.datetime.now())})
218
- script_variables.update(
219
- {"__threads__": str(int(os.cpu_count() * 0.75) if int(os.cpu_count() * 0.75) < 16 else 16)}
220
- )
221
- script_variables.update({"__arch__": arch()})
220
+ script_variables = {
221
+ "__image_id__": unit.id,
222
+ "__name__": unit.name,
223
+ "__version__": str(unit.version),
224
+ "__version_major__": str(unit.version.major),
225
+ "__version_minor__": str(unit.version.minor),
226
+ "__version_patch__": str(unit.version.patch),
227
+ "__version_suffix__": str(unit.version.suffix),
228
+ }
222
229
  if src_image is not None:
223
230
  script_variables.update({"__base__": src_image})
224
231
  script_variables.update(self.variables)
232
+ script_variables.update(unit.variables)
225
233
 
226
234
  script = self.backend_engine.generate_script(unit, script_variables)
227
235
  # write out script
@@ -231,6 +239,15 @@ class ImageBuilder(metaclass=OurMeta):
231
239
  indent_print([TextBlock(line, fore=Fore.BLUE, style=Style.DIM)])
232
240
  out_file.writelines(line + "\n")
233
241
 
242
+ # write out variables
243
+ variables_file = build_sub_dir.joinpath("variables")
244
+ with open(variables_file, "w") as fo:
245
+ for k, v in script_variables.items():
246
+ if v == "None":
247
+ fo.write("export {}=''".format(k) + "\n")
248
+ else:
249
+ fo.write("export {}={}".format(k, v) + "\n")
250
+
234
251
  # run prolog & build
235
252
  build_cmd = self.backend_engine.generate_build_cmd(
236
253
  str(Path.joinpath(build_sub_dir, "script")), name, unit.arguments
@@ -245,12 +262,14 @@ class ImageBuilder(metaclass=OurMeta):
245
262
  TextBlock(": RUNNING PROLOG ... && BUILDING ..."),
246
263
  ]
247
264
  )
248
- build_contents.append(unit.prolog.strip("\n"))
249
- build_contents.append(build_cmd)
250
-
265
+ build_contents.extend(["### VARIABLES ###", f"source {variables_file.absolute()}"])
266
+ build_contents.append("### PROLOG ###")
267
+ build_contents.extend(unit.prolog.strip("\n").split("\n"))
251
268
  else:
252
269
  header_print([TextBlock(unit.id, fore=Fore.RED, style=Style.BRIGHT), TextBlock(": BUILDING ...")])
253
- build_contents.append(build_cmd)
270
+
271
+ build_contents.append("### BUILD ###")
272
+ build_contents.extend(build_cmd)
254
273
 
255
274
  with open(build_file_path, "w") as build_file:
256
275
  for line in build_contents:
@@ -279,7 +298,7 @@ class ImageBuilder(metaclass=OurMeta):
279
298
  TextBlock(" ("),
280
299
  TextBlock("{}@{}".format(unit.name, unit.version), fore=Fore.MAGENTA, style=Style.BRIGHT),
281
300
  TextBlock(") BUILT ["),
282
- TextBlock(str(datetime.timedelta(seconds=round(end - start))), fore=Fore.MAGENTA, style=Style.BRIGHT),
301
+ TextBlock(str(timedelta(seconds=round(end - start))), fore=Fore.MAGENTA, style=Style.BRIGHT),
283
302
  TextBlock("]"),
284
303
  ]
285
304
  )
@@ -3,7 +3,8 @@
3
3
  from loguru import logger
4
4
  from platform import processor as arch
5
5
  from pathlib import Path
6
- from os import getlogin as get_username, getenv
6
+ from os import getenv
7
+ from getpass import getuser
7
8
  from yaml import safe_load as yaml_safe_load
8
9
  from ._exceptions import InvalidConfigIdentifier
9
10
  from ._tools import OurMeta
@@ -99,6 +100,9 @@ if getenv("VELOCITY_IMAGE_PATH") is not None:
99
100
  if getenv("VELOCITY_BUILD_DIR") is not None:
100
101
  _config.set("velocity:build_dir", getenv("VELOCITY_BUILD_DIR"))
101
102
 
103
+ if getenv("VELOCITY_LOGGING_LEVEL") is not None:
104
+ _config.set("velocity:logging:level", getenv("VELOCITY_LOGGING_LEVEL"))
105
+
102
106
  # set defaults for un-configured items
103
107
  if _config.get("velocity:system", warn_on_miss=False) is None:
104
108
  _config.set("velocity:system", arch())
@@ -118,7 +122,7 @@ if _config.get("velocity:image_path", warn_on_miss=False) is None:
118
122
  _config.set("velocity:image_path", image_dir.__str__())
119
123
 
120
124
  if _config.get("velocity:build_dir", warn_on_miss=False) is None:
121
- _config.set("velocity:build_dir", Path("/tmp").joinpath(get_username(), "velocity").__str__())
125
+ _config.set("velocity:build_dir", Path("/tmp").joinpath(getuser(), "velocity").__str__())
122
126
 
123
127
  # export
124
128
  config = _config
@@ -1,14 +1,13 @@
1
1
  """Misc tools."""
2
2
 
3
+ from abc import ABCMeta
3
4
  from functools import wraps
4
- from inspect import isfunction, stack, getmodulename
5
+ from inspect import isfunction
5
6
  from loguru import logger
6
- from re import fullmatch as re_fullmatch
7
- from abc import ABCMeta
7
+ from re import compile as re_compile
8
8
 
9
9
 
10
- # this variable holds the current trace indent
11
- current_trace_indent: list = [0, ]
10
+ builtin_regex = re_compile(r"^__\w+__$")
12
11
 
13
12
 
14
13
  def trace_function(_function):
@@ -27,46 +26,29 @@ def trace_function(_function):
27
26
  # update call number
28
27
  wrapper.call_count = wrapper.call_count + 1
29
28
 
30
- # pre-call update indent
31
- current_trace_indent[0] = current_trace_indent[0] + 1
32
-
33
- # get caller info
34
- current_stack = stack()[1]
35
- calling_lineno: int = current_stack.lineno
36
- calling_module: str = "velocity.{}".format(getmodulename(current_stack.filename))
37
-
38
- # construct trace message (note: 'this_call_depth' will always be >= 1)
39
- depth_str: str = "|{}|".format(" " * (current_trace_indent[0] * 2))
29
+ # construct trace message
40
30
  q_name_str: str = "function '{}:{} {}'".format(
41
- _function.__module__,
42
- _function.__code__.co_firstlineno,
43
- _function.__qualname__
31
+ _function.__module__, _function.__code__.co_firstlineno, _function.__qualname__
44
32
  )
45
- call_number_str: str = "call #{} from {}:{} ".format(wrapper.call_count, calling_module, calling_lineno)
46
- argument_names: tuple[str] = _function.__code__.co_varnames[:_function.__code__.co_argcount]
33
+ call_number_str: str = "call #{} ".format(wrapper.call_count)
34
+ argument_names: tuple[str] = _function.__code__.co_varnames[: _function.__code__.co_argcount]
47
35
  argument_str: str = ""
48
36
  for a in range(len(args)):
49
37
  if argument_names[a] == "self":
50
38
  argument_str += "self, "
51
39
  else:
52
- argument_str += "{}: {} ='{}', ".format(argument_names[a], type(args[a]).__name__, args[a])
40
+ argument_str += "{}: {} = '{}', ".format(argument_names[a], type(args[a]).__name__, args[a])
53
41
  for a in argument_names:
54
42
  if a in kwargs:
55
- argument_str += "{}: {} ='{}', ".format(a, type(kwargs[a]).__name__, kwargs[a])
43
+ argument_str += "{}: {} = '{}', ".format(a, type(kwargs[a]).__name__, kwargs[a])
56
44
  argument_str = "with ({})".format(argument_str)
57
- argument_str = argument_str.replace("\x1b", "\\x1b") # neutralize color escape sequences
45
+ argument_str = argument_str.replace("\x1b", "\\x1b") # neutralize color escape sequences
58
46
 
59
47
  # log trace
60
- logger.trace("{} {} {} {}".format(depth_str, q_name_str, call_number_str, argument_str))
48
+ logger.opt(depth=1).trace("{} {} {}".format(q_name_str, call_number_str, argument_str))
61
49
 
62
- # call _function
63
- result = _function(*args, **kwargs)
64
-
65
- # post-call update indent
66
- current_trace_indent[0] = current_trace_indent[0] - 1
67
-
68
- # return results of calling _function
69
- return result
50
+ # call inner function
51
+ return _function(*args, **kwargs)
70
52
 
71
53
  # set the wrapper call count
72
54
  wrapper.call_count = 0
@@ -78,9 +60,10 @@ def trace_function(_function):
78
60
  class OurMeta(type):
79
61
  """Metaclass to wrap class methods with @trace_function. We do not trace builtin functions
80
62
  in the form __\w+__ except for __init__."""
63
+
81
64
  def __new__(cls, *args, **kwargs):
82
65
  for name, value in args[2].items():
83
- if isfunction(value) and (not re_fullmatch(r"^__\w+__$", name) or name == "__init__"):
66
+ if isfunction(value) and (not builtin_regex.fullmatch(name) or name == "__init__"):
84
67
  args[2][name] = trace_function(args[2][name])
85
68
  return super().__new__(cls, args[0], args[1], args[2])
86
69
 
@@ -88,8 +71,9 @@ class OurMeta(type):
88
71
  class OurABCMeta(ABCMeta):
89
72
  """Metaclass to wrap class methods with @trace_function and inherit from ABCMeta. We do not trace builtin
90
73
  functions in the form __\w+__ except for __init__."""
74
+
91
75
  def __new__(cls, *args, **kwargs):
92
76
  for name, value in args[2].items():
93
- if isfunction(value) and (not re_fullmatch(r"^__\w+__$", name) or name == "__init__"):
77
+ if isfunction(value) and (not builtin_regex.fullmatch(name) or name == "__init__"):
94
78
  args[2][name] = trace_function(args[2][name])
95
79
  return super().__new__(cls, args[0], args[1], args[2])
@@ -1,31 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: olcf-velocity
3
- Version: 0.2.dev0
4
- Summary: A container build manager
5
- Project-URL: Homepage, https://github.com/olcf/velocity
6
- Project-URL: Issues, https://github.com/olcf/velocity/issues
7
- Requires-Python: >=3.10
8
- Description-Content-Type: text/markdown
9
- License-File: LICENSE.txt
10
- Requires-Dist: pyyaml
11
- Requires-Dist: networkx
12
- Requires-Dist: colorama
13
- Requires-Dist: loguru
14
- Requires-Dist: typing_extensions
15
-
16
- ![icon.drawio.png](misc/artwork/icon_name.drawio.png)
17
-
18
- -----------------------------------------------------------
19
-
20
- ## Description
21
- Velocity is a tool to help with the maintenance of container build scripts on
22
- multiple systems, backends (e.g podman or apptainer) and distros.
23
-
24
- ## Documentation
25
- See <https://olcf.github.io/velocity/>.
26
-
27
- ## Installation
28
- ``` commandline
29
- pip install olcf-velocity
30
- alias velocity="python3 -m velocity"
31
- ```
@@ -1,16 +0,0 @@
1
- ![icon.drawio.png](misc/artwork/icon_name.drawio.png)
2
-
3
- -----------------------------------------------------------
4
-
5
- ## Description
6
- Velocity is a tool to help with the maintenance of container build scripts on
7
- multiple systems, backends (e.g podman or apptainer) and distros.
8
-
9
- ## Documentation
10
- See <https://olcf.github.io/velocity/>.
11
-
12
- ## Installation
13
- ``` commandline
14
- pip install olcf-velocity
15
- alias velocity="python3 -m velocity"
16
- ```
@@ -1,31 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: olcf-velocity
3
- Version: 0.2.dev0
4
- Summary: A container build manager
5
- Project-URL: Homepage, https://github.com/olcf/velocity
6
- Project-URL: Issues, https://github.com/olcf/velocity/issues
7
- Requires-Python: >=3.10
8
- Description-Content-Type: text/markdown
9
- License-File: LICENSE.txt
10
- Requires-Dist: pyyaml
11
- Requires-Dist: networkx
12
- Requires-Dist: colorama
13
- Requires-Dist: loguru
14
- Requires-Dist: typing_extensions
15
-
16
- ![icon.drawio.png](misc/artwork/icon_name.drawio.png)
17
-
18
- -----------------------------------------------------------
19
-
20
- ## Description
21
- Velocity is a tool to help with the maintenance of container build scripts on
22
- multiple systems, backends (e.g podman or apptainer) and distros.
23
-
24
- ## Documentation
25
- See <https://olcf.github.io/velocity/>.
26
-
27
- ## Installation
28
- ``` commandline
29
- pip install olcf-velocity
30
- alias velocity="python3 -m velocity"
31
- ```
@@ -1,5 +0,0 @@
1
- from loguru import logger; logger.disable("velocity") # disable logging at the module level
2
- from ._config import config
3
-
4
-
5
- __all__ = [config]